maxGraph/javascript/mxClient.js

90368 lines
2.1 MiB

/**
* Copyright (c) 2006-2017, JGraph Ltd
* Copyright (c) 2006-2017, Gaudenz Alder
*/
var mxClient =
{
/**
* Class: mxClient
*
* Bootstrapping mechanism for the mxGraph thin client. The production version
* of this file contains all code required to run the mxGraph thin client, as
* well as global constants to identify the browser and operating system in
* use. You may have to load chrome://global/content/contentAreaUtils.js in
* your page to disable certain security restrictions in Mozilla.
*
* Variable: VERSION
*
* Contains the current version of the mxGraph library. The strings that
* communicate versions of mxGraph use the following format.
*
* versionMajor.versionMinor.buildNumber.revisionNumber
*
* Current version is 4.0.6.
*/
VERSION: '4.0.6',
/**
* Variable: IS_IE
*
* True if the current browser is Internet Explorer 10 or below. Use <mxClient.IS_IE11>
* to detect IE 11.
*/
IS_IE: navigator.userAgent.indexOf('MSIE') >= 0,
/**
* Variable: IS_IE6
*
* True if the current browser is Internet Explorer 6.x.
*/
IS_IE6: navigator.userAgent.indexOf('MSIE 6') >= 0,
/**
* Variable: IS_IE11
*
* True if the current browser is Internet Explorer 11.x.
*/
IS_IE11: !!navigator.userAgent.match(/Trident\/7\./),
/**
* Variable: IS_EDGE
*
* True if the current browser is Microsoft Edge.
*/
IS_EDGE: !!navigator.userAgent.match(/Edge\//),
/**
* Variable: IS_QUIRKS
*
* True if the current browser is Internet Explorer and it is in quirks mode.
*/
IS_QUIRKS: navigator.userAgent.indexOf('MSIE') >= 0 && (document.documentMode == null || document.documentMode == 5),
/**
* Variable: IS_EM
*
* True if the browser is IE11 in enterprise mode (IE8 standards mode).
*/
IS_EM: 'spellcheck' in document.createElement('textarea') && document.documentMode == 8,
/**
* Variable: VML_PREFIX
*
* Prefix for VML namespace in node names. Default is 'v'.
*/
VML_PREFIX: 'v',
/**
* Variable: OFFICE_PREFIX
*
* Prefix for VML office namespace in node names. Default is 'o'.
*/
OFFICE_PREFIX: 'o',
/**
* Variable: IS_NS
*
* True if the current browser is Netscape (including Firefox).
*/
IS_NS: navigator.userAgent.indexOf('Mozilla/') >= 0 &&
navigator.userAgent.indexOf('MSIE') < 0 &&
navigator.userAgent.indexOf('Edge/') < 0,
/**
* Variable: IS_OP
*
* True if the current browser is Opera.
*/
IS_OP: navigator.userAgent.indexOf('Opera/') >= 0 ||
navigator.userAgent.indexOf('OPR/') >= 0,
/**
* Variable: IS_OT
*
* True if -o-transform is available as a CSS style, ie for Opera browsers
* based on a Presto engine with version 2.5 or later.
*/
IS_OT: navigator.userAgent.indexOf('Presto/') >= 0 &&
navigator.userAgent.indexOf('Presto/2.4.') < 0 &&
navigator.userAgent.indexOf('Presto/2.3.') < 0 &&
navigator.userAgent.indexOf('Presto/2.2.') < 0 &&
navigator.userAgent.indexOf('Presto/2.1.') < 0 &&
navigator.userAgent.indexOf('Presto/2.0.') < 0 &&
navigator.userAgent.indexOf('Presto/1.') < 0,
/**
* Variable: IS_SF
*
* True if the current browser is Safari.
*/
IS_SF: navigator.userAgent.indexOf('AppleWebKit/') >= 0 &&
navigator.userAgent.indexOf('Chrome/') < 0 &&
navigator.userAgent.indexOf('Edge/') < 0,
/**
* Variable: IS_IOS
*
* Returns true if the user agent is an iPad, iPhone or iPod.
*/
IS_IOS: (navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false),
/**
* Variable: IS_GC
*
* True if the current browser is Google Chrome.
*/
IS_GC: navigator.userAgent.indexOf('Chrome/') >= 0 &&
navigator.userAgent.indexOf('Edge/') < 0,
/**
* Variable: IS_CHROMEAPP
*
* True if the this is running inside a Chrome App.
*/
IS_CHROMEAPP: window.chrome != null && chrome.app != null && chrome.app.runtime != null,
/**
* Variable: IS_FF
*
* True if the current browser is Firefox.
*/
IS_FF: navigator.userAgent.indexOf('Firefox/') >= 0,
/**
* Variable: IS_MT
*
* True if -moz-transform is available as a CSS style. This is the case
* for all Firefox-based browsers newer than or equal 3, such as Camino,
* Iceweasel, Seamonkey and Iceape.
*/
IS_MT: (navigator.userAgent.indexOf('Firefox/') >= 0 &&
navigator.userAgent.indexOf('Firefox/1.') < 0 &&
navigator.userAgent.indexOf('Firefox/2.') < 0) ||
(navigator.userAgent.indexOf('Iceweasel/') >= 0 &&
navigator.userAgent.indexOf('Iceweasel/1.') < 0 &&
navigator.userAgent.indexOf('Iceweasel/2.') < 0) ||
(navigator.userAgent.indexOf('SeaMonkey/') >= 0 &&
navigator.userAgent.indexOf('SeaMonkey/1.') < 0) ||
(navigator.userAgent.indexOf('Iceape/') >= 0 &&
navigator.userAgent.indexOf('Iceape/1.') < 0),
/**
* Variable: IS_VML
*
* True if the browser supports VML.
*/
IS_VML: navigator.appName.toUpperCase() == 'MICROSOFT INTERNET EXPLORER',
/**
* Variable: IS_SVG
*
* True if the browser supports SVG.
*/
IS_SVG: navigator.appName.toUpperCase() != 'MICROSOFT INTERNET EXPLORER',
/**
* Variable: NO_FO
*
* True if foreignObject support is not available. This is the case for
* Opera, older SVG-based browsers and all versions of IE.
*/
NO_FO: !document.createElementNS || document.createElementNS('http://www.w3.org/2000/svg',
'foreignObject') != '[object SVGForeignObjectElement]' || navigator.userAgent.indexOf('Opera/') >= 0,
/**
* Variable: IS_WIN
*
* True if the client is a Windows.
*/
IS_WIN: navigator.appVersion.indexOf('Win') > 0,
/**
* Variable: IS_MAC
*
* True if the client is a Mac.
*/
IS_MAC: navigator.appVersion.indexOf('Mac') > 0,
/**
* Variable: IS_CHROMEOS
*
* True if the client is a Chrome OS.
*/
IS_CHROMEOS: /\bCrOS\b/.test(navigator.userAgent),
/**
* Variable: IS_TOUCH
*
* True if this device supports touchstart/-move/-end events (Apple iOS,
* Android, Chromebook and Chrome Browser on touch-enabled devices).
*/
IS_TOUCH: 'ontouchstart' in document.documentElement,
/**
* Variable: IS_POINTER
*
* True if this device supports Microsoft pointer events (always false on Macs).
*/
IS_POINTER: window.PointerEvent != null && !(navigator.appVersion.indexOf('Mac') > 0),
/**
* Variable: IS_LOCAL
*
* True if the documents location does not start with http:// or https://.
*/
IS_LOCAL: document.location.href.indexOf('http://') < 0 &&
document.location.href.indexOf('https://') < 0,
/**
* Variable: defaultBundles
*
* Contains the base names of the default bundles if mxLoadResources is false.
*/
defaultBundles: [],
/**
* Function: isBrowserSupported
*
* Returns true if the current browser is supported, that is, if
* <mxClient.IS_VML> or <mxClient.IS_SVG> is true.
*
* Example:
*
* (code)
* if (!mxClient.isBrowserSupported())
* {
* mxUtils.error('Browser is not supported!', 200, false);
* }
* (end)
*/
isBrowserSupported: function()
{
return mxClient.IS_VML || mxClient.IS_SVG;
},
/**
* Function: link
*
* Adds a link node to the head of the document. Use this
* to add a stylesheet to the page as follows:
*
* (code)
* mxClient.link('stylesheet', filename);
* (end)
*
* where filename is the (relative) URL of the stylesheet. The charset
* is hardcoded to ISO-8859-1 and the type is text/css.
*
* Parameters:
*
* rel - String that represents the rel attribute of the link node.
* href - String that represents the href attribute of the link node.
* doc - Optional parent document of the link node.
* id - unique id for the link element to check if it already exists
*/
link: function(rel, href, doc, id)
{
doc = doc || document;
// Workaround for Operation Aborted in IE6 if base tag is used in head
if (mxClient.IS_IE6)
{
doc.write('<link rel="' + rel + '" href="' + href + '" charset="UTF-8" type="text/css"/>');
}
else
{
var link = doc.createElement('link');
link.setAttribute('rel', rel);
link.setAttribute('href', href);
link.setAttribute('charset', 'UTF-8');
link.setAttribute('type', 'text/css');
if (id)
{
link.setAttribute('id', id);
}
var head = doc.getElementsByTagName('head')[0];
head.appendChild(link);
}
},
/**
* Function: loadResources
*
* Helper method to load the default bundles if mxLoadResources is false.
*
* Parameters:
*
* fn - Function to call after all resources have been loaded.
* lan - Optional string to pass to <mxResources.add>.
*/
loadResources: function(fn, lan)
{
var pending = mxClient.defaultBundles.length;
function callback()
{
if (--pending == 0)
{
fn();
}
}
for (var i = 0; i < mxClient.defaultBundles.length; i++)
{
mxResources.add(mxClient.defaultBundles[i], lan, callback);
}
},
/**
* Function: include
*
* Dynamically adds a script node to the document header.
*
* In production environments, the includes are resolved in the mxClient.js
* file to reduce the number of requests required for client startup. This
* function should only be used in development environments, but not in
* production systems.
*/
include: function(src)
{
document.write('<script src="'+src+'"></script>');
}
};
/**
* Variable: mxLoadResources
*
* Optional global config variable to toggle loading of the two resource files
* in <mxGraph> and <mxEditor>. Default is true. NOTE: This is a global variable,
* not a variable of mxClient. If this is false, you can use <mxClient.loadResources>
* with its callback to load the default bundles asynchronously.
*
* (code)
* <script type="text/javascript">
* var mxLoadResources = false;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxLoadResources) == 'undefined')
{
mxLoadResources = true;
}
/**
* Variable: mxForceIncludes
*
* Optional global config variable to force loading the JavaScript files in
* development mode. Default is undefined. NOTE: This is a global variable,
* not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxLoadResources = true;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxForceIncludes) == 'undefined')
{
mxForceIncludes = false;
}
/**
* Variable: mxResourceExtension
*
* Optional global config variable to specify the extension of resource files.
* Default is true. NOTE: This is a global variable, not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxResourceExtension = '.txt';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxResourceExtension) == 'undefined')
{
mxResourceExtension = '.txt';
}
/**
* Variable: mxLoadStylesheets
*
* Optional global config variable to toggle loading of the CSS files when
* the library is initialized. Default is true. NOTE: This is a global variable,
* not a variable of mxClient.
*
* (code)
* <script type="text/javascript">
* var mxLoadStylesheets = false;
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*/
if (typeof(mxLoadStylesheets) == 'undefined')
{
mxLoadStylesheets = true;
}
/**
* Variable: basePath
*
* Basepath for all URLs in the core without trailing slash. Default is '.'.
* Set mxBasePath prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxBasePath = '/path/to/core/directory';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*
* When using a relative path, the path is relative to the URL of the page that
* contains the assignment. Trailing slashes are automatically removed.
*/
if (typeof(mxBasePath) != 'undefined' && mxBasePath.length > 0)
{
// Adds a trailing slash if required
if (mxBasePath.substring(mxBasePath.length - 1) == '/')
{
mxBasePath = mxBasePath.substring(0, mxBasePath.length - 1);
}
mxClient.basePath = mxBasePath;
}
else
{
mxClient.basePath = '.';
}
/**
* Variable: imageBasePath
*
* Basepath for all images URLs in the core without trailing slash. Default is
* <mxClient.basePath> + '/images'. Set mxImageBasePath prior to loading the
* mxClient library as follows to override this setting:
*
* (code)
* <script type="text/javascript">
* mxImageBasePath = '/path/to/image/directory';
* </script>
* <script type="text/javascript" src="/path/to/core/directory/js/mxClient.js"></script>
* (end)
*
* When using a relative path, the path is relative to the URL of the page that
* contains the assignment. Trailing slashes are automatically removed.
*/
if (typeof(mxImageBasePath) != 'undefined' && mxImageBasePath.length > 0)
{
// Adds a trailing slash if required
if (mxImageBasePath.substring(mxImageBasePath.length - 1) == '/')
{
mxImageBasePath = mxImageBasePath.substring(0, mxImageBasePath.length - 1);
}
mxClient.imageBasePath = mxImageBasePath;
}
else
{
mxClient.imageBasePath = mxClient.basePath + '/images';
}
/**
* Variable: language
*
* Defines the language of the client, eg. en for english, de for german etc.
* The special value 'none' will disable all built-in internationalization and
* resource loading. See <mxResources.getSpecialBundle> for handling identifiers
* with and without a dash.
*
* Set mxLanguage prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxLanguage = 'en';
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*
* If internationalization is disabled, then the following variables should be
* overridden to reflect the current language of the system. These variables are
* cleared when i18n is disabled.
* <mxEditor.askZoomResource>, <mxEditor.lastSavedResource>,
* <mxEditor.currentFileResource>, <mxEditor.propertiesResource>,
* <mxEditor.tasksResource>, <mxEditor.helpResource>, <mxEditor.outlineResource>,
* <mxElbowEdgeHandler.doubleClickOrientationResource>, <mxUtils.errorResource>,
* <mxUtils.closeResource>, <mxGraphSelectionModel.doneResource>,
* <mxGraphSelectionModel.updatingSelectionResource>, <mxGraphView.doneResource>,
* <mxGraphView.updatingDocumentResource>, <mxCellRenderer.collapseExpandResource>,
* <mxGraph.containsValidationErrorsResource> and
* <mxGraph.alreadyConnectedResource>.
*/
if (typeof(mxLanguage) != 'undefined' && mxLanguage != null)
{
mxClient.language = mxLanguage;
}
else
{
mxClient.language = (mxClient.IS_IE) ? navigator.userLanguage : navigator.language;
}
/**
* Variable: defaultLanguage
*
* Defines the default language which is used in the common resource files. Any
* resources for this language will only load the common resource file, but not
* the language-specific resource file. Default is 'en'.
*
* Set mxDefaultLanguage prior to loading the mxClient library as follows to override
* this setting:
*
* (code)
* <script type="text/javascript">
* mxDefaultLanguage = 'de';
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*/
if (typeof(mxDefaultLanguage) != 'undefined' && mxDefaultLanguage != null)
{
mxClient.defaultLanguage = mxDefaultLanguage;
}
else
{
mxClient.defaultLanguage = 'en';
}
// Adds all required stylesheets and namespaces
if (mxLoadStylesheets)
{
mxClient.link('stylesheet', mxClient.basePath + '/css/common.css');
}
/**
* Variable: languages
*
* Defines the optional array of all supported language extensions. The default
* language does not have to be part of this list. See
* <mxResources.isLanguageSupported>.
*
* (code)
* <script type="text/javascript">
* mxLanguages = ['de', 'it', 'fr'];
* </script>
* <script type="text/javascript" src="js/mxClient.js"></script>
* (end)
*
* This is used to avoid unnecessary requests to language files, ie. if a 404
* will be returned.
*/
if (typeof(mxLanguages) != 'undefined' && mxLanguages != null)
{
mxClient.languages = mxLanguages;
}
// Adds required namespaces, stylesheets and memory handling for older IE browsers
if (mxClient.IS_VML)
{
if (mxClient.IS_SVG)
{
mxClient.IS_VML = false;
}
else
{
// Enables support for IE8 standards mode. Note that this requires all attributes for VML
// elements to be set using direct notation, ie. node.attr = value. The use of setAttribute
// is not possible.
if (document.documentMode == 8)
{
document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml', '#default#VML');
document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office', '#default#VML');
}
else
{
document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml');
document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office');
}
// Workaround for limited number of stylesheets in IE (does not work in standards mode)
if (mxClient.IS_QUIRKS && document.styleSheets.length >= 30)
{
(function()
{
var node = document.createElement('style');
node.type = 'text/css';
node.styleSheet.cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' +
mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}';
document.getElementsByTagName('head')[0].appendChild(node);
})();
}
else
{
document.createStyleSheet().cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' +
mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}';
}
if (mxLoadStylesheets)
{
mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css');
}
}
}
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxLog =
{
/**
* Class: mxLog
*
* A singleton class that implements a simple console.
*
* Variable: consoleName
*
* Specifies the name of the console window. Default is 'Console'.
*/
consoleName: 'Console',
/**
* Variable: TRACE
*
* Specified if the output for <enter> and <leave> should be visible in the
* console. Default is false.
*/
TRACE: false,
/**
* Variable: DEBUG
*
* Specifies if the output for <debug> should be visible in the console.
* Default is true.
*/
DEBUG: true,
/**
* Variable: WARN
*
* Specifies if the output for <warn> should be visible in the console.
* Default is true.
*/
WARN: true,
/**
* Variable: buffer
*
* Buffer for pre-initialized content.
*/
buffer: '',
/**
* Function: init
*
* Initializes the DOM node for the console. This requires document.body to
* point to a non-null value. This is called from within <setVisible> if the
* log has not yet been initialized.
*/
init: function()
{
if (mxLog.window == null && document.body != null)
{
var title = mxLog.consoleName + ' - mxGraph ' + mxClient.VERSION;
// Creates a table that maintains the layout
var table = document.createElement('table');
table.setAttribute('width', '100%');
table.setAttribute('height', '100%');
var tbody = document.createElement('tbody');
var tr = document.createElement('tr');
var td = document.createElement('td');
td.style.verticalAlign = 'top';
// Adds the actual console as a textarea
mxLog.textarea = document.createElement('textarea');
mxLog.textarea.setAttribute('wrap', 'off');
mxLog.textarea.setAttribute('readOnly', 'true');
mxLog.textarea.style.height = '100%';
mxLog.textarea.style.resize = 'none';
mxLog.textarea.value = mxLog.buffer;
// Workaround for wrong width in standards mode
if (mxClient.IS_NS && document.compatMode != 'BackCompat')
{
mxLog.textarea.style.width = '99%';
}
else
{
mxLog.textarea.style.width = '100%';
}
td.appendChild(mxLog.textarea);
tr.appendChild(td);
tbody.appendChild(tr);
// Creates the container div
tr = document.createElement('tr');
mxLog.td = document.createElement('td');
mxLog.td.style.verticalAlign = 'top';
mxLog.td.setAttribute('height', '30px');
tr.appendChild(mxLog.td);
tbody.appendChild(tr);
table.appendChild(tbody);
// Adds various debugging buttons
mxLog.addButton('Info', function (evt)
{
mxLog.info();
});
mxLog.addButton('DOM', function (evt)
{
var content = mxUtils.getInnerHtml(document.body);
mxLog.debug(content);
});
mxLog.addButton('Trace', function (evt)
{
mxLog.TRACE = !mxLog.TRACE;
if (mxLog.TRACE)
{
mxLog.debug('Tracing enabled');
}
else
{
mxLog.debug('Tracing disabled');
}
});
mxLog.addButton('Copy', function (evt)
{
try
{
mxUtils.copy(mxLog.textarea.value);
}
catch (err)
{
mxUtils.alert(err);
}
});
mxLog.addButton('Show', function (evt)
{
try
{
mxUtils.popup(mxLog.textarea.value);
}
catch (err)
{
mxUtils.alert(err);
}
});
mxLog.addButton('Clear', function (evt)
{
mxLog.textarea.value = '';
});
// Cross-browser code to get window size
var h = 0;
var w = 0;
if (typeof(window.innerWidth) === 'number')
{
h = window.innerHeight;
w = window.innerWidth;
}
else
{
h = (document.documentElement.clientHeight || document.body.clientHeight);
w = document.body.clientWidth;
}
mxLog.window = new mxWindow(title, table, Math.max(0, w - 320), Math.max(0, h - 210), 300, 160);
mxLog.window.setMaximizable(true);
mxLog.window.setScrollable(false);
mxLog.window.setResizable(true);
mxLog.window.setClosable(true);
mxLog.window.destroyOnClose = false;
// Workaround for ignored textarea height in various setups
if (((mxClient.IS_NS || mxClient.IS_IE) && !mxClient.IS_GC &&
!mxClient.IS_SF && document.compatMode != 'BackCompat') ||
document.documentMode == 11)
{
var elt = mxLog.window.getElement();
var resizeHandler = function(sender, evt)
{
mxLog.textarea.style.height = Math.max(0, elt.offsetHeight - 70) + 'px';
};
mxLog.window.addListener(mxEvent.RESIZE_END, resizeHandler);
mxLog.window.addListener(mxEvent.MAXIMIZE, resizeHandler);
mxLog.window.addListener(mxEvent.NORMALIZE, resizeHandler);
mxLog.textarea.style.height = '92px';
}
}
},
/**
* Function: info
*
* Writes the current navigator information to the console.
*/
info: function()
{
mxLog.writeln(mxUtils.toString(navigator));
},
/**
* Function: addButton
*
* Adds a button to the console using the given label and function.
*/
addButton: function(lab, funct)
{
var button = document.createElement('button');
mxUtils.write(button, lab);
mxEvent.addListener(button, 'click', funct);
mxLog.td.appendChild(button);
},
/**
* Function: isVisible
*
* Returns true if the console is visible.
*/
isVisible: function()
{
if (mxLog.window != null)
{
return mxLog.window.isVisible();
}
return false;
},
/**
* Function: show
*
* Shows the console.
*/
show: function()
{
mxLog.setVisible(true);
},
/**
* Function: setVisible
*
* Shows or hides the console.
*/
setVisible: function(visible)
{
if (mxLog.window == null)
{
mxLog.init();
}
if (mxLog.window != null)
{
mxLog.window.setVisible(visible);
}
},
/**
* Function: enter
*
* Writes the specified string to the console
* if <TRACE> is true and returns the current
* time in milliseconds.
*
* Example:
*
* (code)
* mxLog.show();
* var t0 = mxLog.enter('Hello');
* // Do something
* mxLog.leave('World!', t0);
* (end)
*/
enter: function(string)
{
if (mxLog.TRACE)
{
mxLog.writeln('Entering '+string);
return new Date().getTime();
}
},
/**
* Function: leave
*
* Writes the specified string to the console
* if <TRACE> is true and computes the difference
* between the current time and t0 in milliseconds.
* See <enter> for an example.
*/
leave: function(string, t0)
{
if (mxLog.TRACE)
{
var dt = (t0 != 0) ? ' ('+(new Date().getTime() - t0)+' ms)' : '';
mxLog.writeln('Leaving '+string+dt);
}
},
/**
* Function: debug
*
* Adds all arguments to the console if <DEBUG> is enabled.
*
* Example:
*
* (code)
* mxLog.show();
* mxLog.debug('Hello, World!');
* (end)
*/
debug: function()
{
if (mxLog.DEBUG)
{
mxLog.writeln.apply(this, arguments);
}
},
/**
* Function: warn
*
* Adds all arguments to the console if <WARN> is enabled.
*
* Example:
*
* (code)
* mxLog.show();
* mxLog.warn('Hello, World!');
* (end)
*/
warn: function()
{
if (mxLog.WARN)
{
mxLog.writeln.apply(this, arguments);
}
},
/**
* Function: write
*
* Adds the specified strings to the console.
*/
write: function()
{
var string = '';
for (var i = 0; i < arguments.length; i++)
{
string += arguments[i];
if (i < arguments.length - 1)
{
string += ' ';
}
}
if (mxLog.textarea != null)
{
mxLog.textarea.value = mxLog.textarea.value + string;
// Workaround for no update in Presto 2.5.22 (Opera 10.5)
if (navigator.userAgent.indexOf('Presto/2.5') >= 0)
{
mxLog.textarea.style.visibility = 'hidden';
mxLog.textarea.style.visibility = 'visible';
}
mxLog.textarea.scrollTop = mxLog.textarea.scrollHeight;
}
else
{
mxLog.buffer += string;
}
},
/**
* Function: writeln
*
* Adds the specified strings to the console, appending a linefeed at the
* end of each string.
*/
writeln: function()
{
var string = '';
for (var i = 0; i < arguments.length; i++)
{
string += arguments[i];
if (i < arguments.length - 1)
{
string += ' ';
}
}
mxLog.write(string + '\n');
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxObjectIdentity =
{
/**
* Class: mxObjectIdentity
*
* Identity for JavaScript objects and functions. This is implemented using
* a simple incrementing counter which is stored in each object under
* <FIELD_NAME>.
*
* The identity for an object does not change during its lifecycle.
*
* Variable: FIELD_NAME
*
* Name of the field to be used to store the object ID. Default is
* <code>mxObjectId</code>.
*/
FIELD_NAME: 'mxObjectId',
/**
* Variable: counter
*
* Current counter.
*/
counter: 0,
/**
* Function: get
*
* Returns the ID for the given object or function or null if no object
* is specified.
*/
get: function(obj)
{
if (obj != null)
{
if (obj[mxObjectIdentity.FIELD_NAME] == null)
{
if (typeof obj === 'object')
{
var ctor = mxUtils.getFunctionName(obj.constructor);
obj[mxObjectIdentity.FIELD_NAME] = ctor + '#' + mxObjectIdentity.counter++;
}
else if (typeof obj === 'function')
{
obj[mxObjectIdentity.FIELD_NAME] = 'Function#' + mxObjectIdentity.counter++;
}
}
return obj[mxObjectIdentity.FIELD_NAME];
}
return null;
},
/**
* Function: clear
*
* Deletes the ID from the given object or function.
*/
clear: function(obj)
{
if (typeof(obj) === 'object' || typeof obj === 'function')
{
delete obj[mxObjectIdentity.FIELD_NAME];
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDictionary
*
* A wrapper class for an associative array with object keys. Note: This
* implementation uses <mxObjectIdentitiy> to turn object keys into strings.
*
* Constructor: mxEventSource
*
* Constructs a new dictionary which allows object to be used as keys.
*/
function mxDictionary()
{
this.clear();
};
/**
* Function: map
*
* Stores the (key, value) pairs in this dictionary.
*/
mxDictionary.prototype.map = null;
/**
* Function: clear
*
* Clears the dictionary.
*/
mxDictionary.prototype.clear = function()
{
this.map = {};
};
/**
* Function: get
*
* Returns the value for the given key.
*/
mxDictionary.prototype.get = function(key)
{
var id = mxObjectIdentity.get(key);
return this.map[id];
};
/**
* Function: put
*
* Stores the value under the given key and returns the previous
* value for that key.
*/
mxDictionary.prototype.put = function(key, value)
{
var id = mxObjectIdentity.get(key);
var previous = this.map[id];
this.map[id] = value;
return previous;
};
/**
* Function: remove
*
* Removes the value for the given key and returns the value that
* has been removed.
*/
mxDictionary.prototype.remove = function(key)
{
var id = mxObjectIdentity.get(key);
var previous = this.map[id];
delete this.map[id];
return previous;
};
/**
* Function: getKeys
*
* Returns all keys as an array.
*/
mxDictionary.prototype.getKeys = function()
{
var result = [];
for (var key in this.map)
{
result.push(key);
}
return result;
};
/**
* Function: getValues
*
* Returns all values as an array.
*/
mxDictionary.prototype.getValues = function()
{
var result = [];
for (var key in this.map)
{
result.push(this.map[key]);
}
return result;
};
/**
* Function: visit
*
* Visits all entries in the dictionary using the given function with the
* following signature: function(key, value) where key is a string and
* value is an object.
*
* Parameters:
*
* visitor - A function that takes the key and value as arguments.
*/
mxDictionary.prototype.visit = function(visitor)
{
for (var key in this.map)
{
visitor(key, this.map[key]);
}
};
/**
* Copyright (c) 2006-2016, JGraph Ltd
* Copyright (c) 2006-2016, Gaudenz Alder
*/
var mxResources =
{
/**
* Class: mxResources
*
* Implements internationalization. You can provide any number of
* resource files on the server using the following format for the
* filename: name[-en].properties. The en stands for any lowercase
* 2-character language shortcut (eg. de for german, fr for french).
*
* If the optional language extension is omitted, then the file is used as a
* default resource which is loaded in all cases. If a properties file for a
* specific language exists, then it is used to override the settings in the
* default resource. All entries in the file are of the form key=value. The
* values may then be accessed in code via <get>. Lines without
* equal signs in the properties files are ignored.
*
* Resource files may either be added programmatically using
* <add> or via a resource tag in the UI section of the
* editor configuration file, eg:
*
* (code)
* <mxEditor>
* <ui>
* <resource basename="examples/resources/mxWorkflow"/>
* (end)
*
* The above element will load examples/resources/mxWorkflow.properties as well
* as the language specific file for the current language, if it exists.
*
* Values may contain placeholders of the form {1}...{n} where each placeholder
* is replaced with the value of the corresponding array element in the params
* argument passed to <mxResources.get>. The placeholder {1} maps to the first
* element in the array (at index 0).
*
* See <mxClient.language> for more information on specifying the default
* language or disabling all loading of resources.
*
* Lines that start with a # sign will be ignored.
*
* Special characters
*
* To use unicode characters, use the standard notation (eg. \u8fd1) or %u as a
* prefix (eg. %u20AC will display a Euro sign). For normal hex encoded strings,
* use % as a prefix, eg. %F6 will display a "o umlaut" (&ouml;).
*
* See <resourcesEncoded> to disable this. If you disable this, make sure that
* your files are UTF-8 encoded.
*
* Asynchronous loading
*
* By default, the core adds two resource files synchronously at load time.
* To load these files asynchronously, set <mxLoadResources> to false
* before loading mxClient.js and use <mxResources.loadResources> instead.
*
* Variable: resources
*
* Object that maps from keys to values.
*/
resources: {},
/**
* Variable: extension
*
* Specifies the extension used for language files. Default is <mxResourceExtension>.
*/
extension: mxResourceExtension,
/**
* Variable: resourcesEncoded
*
* Specifies whether or not values in resource files are encoded with \u or
* percentage. Default is false.
*/
resourcesEncoded: false,
/**
* Variable: loadDefaultBundle
*
* Specifies if the default file for a given basename should be loaded.
* Default is true.
*/
loadDefaultBundle: true,
/**
* Variable: loadDefaultBundle
*
* Specifies if the specific language file file for a given basename should
* be loaded. Default is true.
*/
loadSpecialBundle: true,
/**
* Function: isLanguageSupported
*
* Hook for subclassers to disable support for a given language. This
* implementation returns true if lan is in <mxClient.languages>.
*
* Parameters:
*
* lan - The current language.
*/
isLanguageSupported: function(lan)
{
if (mxClient.languages != null)
{
return mxUtils.indexOf(mxClient.languages, lan) >= 0;
}
return true;
},
/**
* Function: getDefaultBundle
*
* Hook for subclassers to return the URL for the special bundle. This
* implementation returns basename + <extension> or null if
* <loadDefaultBundle> is false.
*
* Parameters:
*
* basename - The basename for which the file should be loaded.
* lan - The current language.
*/
getDefaultBundle: function(basename, lan)
{
if (mxResources.loadDefaultBundle || !mxResources.isLanguageSupported(lan))
{
return basename + mxResources.extension;
}
else
{
return null;
}
},
/**
* Function: getSpecialBundle
*
* Hook for subclassers to return the URL for the special bundle. This
* implementation returns basename + '_' + lan + <extension> or null if
* <loadSpecialBundle> is false or lan equals <mxClient.defaultLanguage>.
*
* If <mxResources.languages> is not null and <mxClient.language> contains
* a dash, then this method checks if <isLanguageSupported> returns true
* for the full language (including the dash). If that returns false the
* first part of the language (up to the dash) will be tried as an extension.
*
* If <mxResources.language> is null then the first part of the language is
* used to maintain backwards compatibility.
*
* Parameters:
*
* basename - The basename for which the file should be loaded.
* lan - The language for which the file should be loaded.
*/
getSpecialBundle: function(basename, lan)
{
if (mxClient.languages == null || !this.isLanguageSupported(lan))
{
var dash = lan.indexOf('-');
if (dash > 0)
{
lan = lan.substring(0, dash);
}
}
if (mxResources.loadSpecialBundle && mxResources.isLanguageSupported(lan) && lan != mxClient.defaultLanguage)
{
return basename + '_' + lan + mxResources.extension;
}
else
{
return null;
}
},
/**
* Function: add
*
* Adds the default and current language properties file for the specified
* basename. Existing keys are overridden as new files are added. If no
* callback is used then the request is synchronous.
*
* Example:
*
* At application startup, additional resources may be
* added using the following code:
*
* (code)
* mxResources.add('resources/editor');
* (end)
*
* Parameters:
*
* basename - The basename for which the file should be loaded.
* lan - The language for which the file should be loaded.
* callback - Optional callback for asynchronous loading.
*/
add: function(basename, lan, callback)
{
lan = (lan != null) ? lan : ((mxClient.language != null) ?
mxClient.language.toLowerCase() : mxConstants.NONE);
if (lan != mxConstants.NONE)
{
var defaultBundle = mxResources.getDefaultBundle(basename, lan);
var specialBundle = mxResources.getSpecialBundle(basename, lan);
var loadSpecialBundle = function()
{
if (specialBundle != null)
{
if (callback)
{
mxUtils.get(specialBundle, function(req)
{
mxResources.parse(req.getText());
callback();
}, function()
{
callback();
});
}
else
{
try
{
var req = mxUtils.load(specialBundle);
if (req.isReady())
{
mxResources.parse(req.getText());
}
}
catch (e)
{
// ignore
}
}
}
else if (callback != null)
{
callback();
}
}
if (defaultBundle != null)
{
if (callback)
{
mxUtils.get(defaultBundle, function(req)
{
mxResources.parse(req.getText());
loadSpecialBundle();
}, function()
{
loadSpecialBundle();
});
}
else
{
try
{
var req = mxUtils.load(defaultBundle);
if (req.isReady())
{
mxResources.parse(req.getText());
}
loadSpecialBundle();
}
catch (e)
{
// ignore
}
}
}
else
{
// Overlays the language specific file (_lan-extension)
loadSpecialBundle();
}
}
},
/**
* Function: parse
*
* Parses the key, value pairs in the specified
* text and stores them as local resources.
*/
parse: function(text)
{
if (text != null)
{
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++)
{
if (lines[i].charAt(0) != '#')
{
var index = lines[i].indexOf('=');
if (index > 0)
{
var key = lines[i].substring(0, index);
var idx = lines[i].length;
if (lines[i].charCodeAt(idx - 1) == 13)
{
idx--;
}
var value = lines[i].substring(index + 1, idx);
if (this.resourcesEncoded)
{
value = value.replace(/\\(?=u[a-fA-F\d]{4})/g,"%");
mxResources.resources[key] = unescape(value);
}
else
{
mxResources.resources[key] = value;
}
}
}
}
}
},
/**
* Function: get
*
* Returns the value for the specified resource key.
*
* Example:
* To read the value for 'welomeMessage', use the following:
* (code)
* var result = mxResources.get('welcomeMessage') || '';
* (end)
*
* This would require an entry of the following form in
* one of the English language resource files:
* (code)
* welcomeMessage=Welcome to mxGraph!
* (end)
*
* The part behind the || is the string value to be used if the given
* resource is not available.
*
* Parameters:
*
* key - String that represents the key of the resource to be returned.
* params - Array of the values for the placeholders of the form {1}...{n}
* to be replaced with in the resulting string.
* defaultValue - Optional string that specifies the default return value.
*/
get: function(key, params, defaultValue)
{
var value = mxResources.resources[key];
// Applies the default value if no resource was found
if (value == null)
{
value = defaultValue;
}
// Replaces the placeholders with the values in the array
if (value != null && params != null)
{
value = mxResources.replacePlaceholders(value, params);
}
return value;
},
/**
* Function: replacePlaceholders
*
* Replaces the given placeholders with the given parameters.
*
* Parameters:
*
* value - String that contains the placeholders.
* params - Array of the values for the placeholders of the form {1}...{n}
* to be replaced with in the resulting string.
*/
replacePlaceholders: function(value, params)
{
var result = [];
var index = null;
for (var i = 0; i < value.length; i++)
{
var c = value.charAt(i);
if (c == '{')
{
index = '';
}
else if (index != null && c == '}')
{
index = parseInt(index)-1;
if (index >= 0 && index < params.length)
{
result.push(params[index]);
}
index = null;
}
else if (index != null)
{
index += c;
}
else
{
result.push(c);
}
}
return result.join('');
},
/**
* Function: loadResources
*
* Loads all required resources asynchronously. Use this to load the graph and
* editor resources if <mxLoadResources> is false.
*
* Parameters:
*
* callback - Callback function for asynchronous loading.
*/
loadResources: function(callback)
{
mxResources.add(mxClient.basePath+'/resources/editor', null, function()
{
mxResources.add(mxClient.basePath+'/resources/graph', null, callback);
});
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPoint
*
* Implements a 2-dimensional vector with double precision coordinates.
*
* Constructor: mxPoint
*
* Constructs a new point for the optional x and y coordinates. If no
* coordinates are given, then the default values for <x> and <y> are used.
*/
function mxPoint(x, y)
{
this.x = (x != null) ? x : 0;
this.y = (y != null) ? y : 0;
};
/**
* Variable: x
*
* Holds the x-coordinate of the point. Default is 0.
*/
mxPoint.prototype.x = null;
/**
* Variable: y
*
* Holds the y-coordinate of the point. Default is 0.
*/
mxPoint.prototype.y = null;
/**
* Function: equals
*
* Returns true if the given object equals this point.
*/
mxPoint.prototype.equals = function(obj)
{
return obj != null && obj.x == this.x && obj.y == this.y;
};
/**
* Function: clone
*
* Returns a clone of this <mxPoint>.
*/
mxPoint.prototype.clone = function()
{
// Handles subclasses as well
return mxUtils.clone(this);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxRectangle
*
* Extends <mxPoint> to implement a 2-dimensional rectangle with double
* precision coordinates.
*
* Constructor: mxRectangle
*
* Constructs a new rectangle for the optional parameters. If no parameters
* are given then the respective default values are used.
*/
function mxRectangle(x, y, width, height)
{
mxPoint.call(this, x, y);
this.width = (width != null) ? width : 0;
this.height = (height != null) ? height : 0;
};
/**
* Extends mxPoint.
*/
mxRectangle.prototype = new mxPoint();
mxRectangle.prototype.constructor = mxRectangle;
/**
* Variable: width
*
* Holds the width of the rectangle. Default is 0.
*/
mxRectangle.prototype.width = null;
/**
* Variable: height
*
* Holds the height of the rectangle. Default is 0.
*/
mxRectangle.prototype.height = null;
/**
* Function: setRect
*
* Sets this rectangle to the specified values
*/
mxRectangle.prototype.setRect = function(x, y, w, h)
{
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
/**
* Function: getCenterX
*
* Returns the x-coordinate of the center point.
*/
mxRectangle.prototype.getCenterX = function ()
{
return this.x + this.width/2;
};
/**
* Function: getCenterY
*
* Returns the y-coordinate of the center point.
*/
mxRectangle.prototype.getCenterY = function ()
{
return this.y + this.height/2;
};
/**
* Function: add
*
* Adds the given rectangle to this rectangle.
*/
mxRectangle.prototype.add = function(rect)
{
if (rect != null)
{
var minX = Math.min(this.x, rect.x);
var minY = Math.min(this.y, rect.y);
var maxX = Math.max(this.x + this.width, rect.x + rect.width);
var maxY = Math.max(this.y + this.height, rect.y + rect.height);
this.x = minX;
this.y = minY;
this.width = maxX - minX;
this.height = maxY - minY;
}
};
/**
* Function: intersect
*
* Changes this rectangle to where it overlaps with the given rectangle.
*/
mxRectangle.prototype.intersect = function(rect)
{
if (rect != null)
{
var r1 = this.x + this.width;
var r2 = rect.x + rect.width;
var b1 = this.y + this.height;
var b2 = rect.y + rect.height;
this.x = Math.max(this.x, rect.x);
this.y = Math.max(this.y, rect.y);
this.width = Math.min(r1, r2) - this.x;
this.height = Math.min(b1, b2) - this.y;
}
};
/**
* Function: grow
*
* Grows the rectangle by the given amount, that is, this method subtracts
* the given amount from the x- and y-coordinates and adds twice the amount
* to the width and height.
*/
mxRectangle.prototype.grow = function(amount)
{
this.x -= amount;
this.y -= amount;
this.width += 2 * amount;
this.height += 2 * amount;
};
/**
* Function: getPoint
*
* Returns the top, left corner as a new <mxPoint>.
*/
mxRectangle.prototype.getPoint = function()
{
return new mxPoint(this.x, this.y);
};
/**
* Function: rotate90
*
* Rotates this rectangle by 90 degree around its center point.
*/
mxRectangle.prototype.rotate90 = function()
{
var t = (this.width - this.height) / 2;
this.x += t;
this.y -= t;
var tmp = this.width;
this.width = this.height;
this.height = tmp;
};
/**
* Function: equals
*
* Returns true if the given object equals this rectangle.
*/
mxRectangle.prototype.equals = function(obj)
{
return obj != null && obj.x == this.x && obj.y == this.y &&
obj.width == this.width && obj.height == this.height;
};
/**
* Function: fromRectangle
*
* Returns a new <mxRectangle> which is a copy of the given rectangle.
*/
mxRectangle.fromRectangle = function(rect)
{
return new mxRectangle(rect.x, rect.y, rect.width, rect.height);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxEffects =
{
/**
* Class: mxEffects
*
* Provides animation effects.
*/
/**
* Function: animateChanges
*
* Asynchronous animated move operation. See also: <mxMorphing>.
*
* Example:
*
* (code)
* graph.model.addListener(mxEvent.CHANGE, function(sender, evt)
* {
* var changes = evt.getProperty('edit').changes;
*
* if (changes.length < 10)
* {
* mxEffects.animateChanges(graph, changes);
* }
* });
* (end)
*
* Parameters:
*
* graph - <mxGraph> that received the changes.
* changes - Array of changes to be animated.
* done - Optional function argument that is invoked after the
* last step of the animation.
*/
animateChanges: function(graph, changes, done)
{
var maxStep = 10;
var step = 0;
var animate = function()
{
var isRequired = false;
for (var i = 0; i < changes.length; i++)
{
var change = changes[i];
if (change instanceof mxGeometryChange ||
change instanceof mxTerminalChange ||
change instanceof mxValueChange ||
change instanceof mxChildChange ||
change instanceof mxStyleChange)
{
var state = graph.getView().getState(change.cell || change.child, false);
if (state != null)
{
isRequired = true;
if (change.constructor != mxGeometryChange || graph.model.isEdge(change.cell))
{
mxUtils.setOpacity(state.shape.node, 100 * step / maxStep);
}
else
{
var scale = graph.getView().scale;
var dx = (change.geometry.x - change.previous.x) * scale;
var dy = (change.geometry.y - change.previous.y) * scale;
var sx = (change.geometry.width - change.previous.width) * scale;
var sy = (change.geometry.height - change.previous.height) * scale;
if (step == 0)
{
state.x -= dx;
state.y -= dy;
state.width -= sx;
state.height -= sy;
}
else
{
state.x += dx / maxStep;
state.y += dy / maxStep;
state.width += sx / maxStep;
state.height += sy / maxStep;
}
graph.cellRenderer.redraw(state);
// Fades all connected edges and children
mxEffects.cascadeOpacity(graph, change.cell, 100 * step / maxStep);
}
}
}
}
if (step < maxStep && isRequired)
{
step++;
window.setTimeout(animate, delay);
}
else if (done != null)
{
done();
}
};
var delay = 30;
animate();
},
/**
* Function: cascadeOpacity
*
* Sets the opacity on the given cell and its descendants.
*
* Parameters:
*
* graph - <mxGraph> that contains the cells.
* cell - <mxCell> to set the opacity for.
* opacity - New value for the opacity in %.
*/
cascadeOpacity: function(graph, cell, opacity)
{
// Fades all children
var childCount = graph.model.getChildCount(cell);
for (var i=0; i<childCount; i++)
{
var child = graph.model.getChildAt(cell, i);
var childState = graph.getView().getState(child);
if (childState != null)
{
mxUtils.setOpacity(childState.shape.node, opacity);
mxEffects.cascadeOpacity(graph, child, opacity);
}
}
// Fades all connected edges
var edges = graph.model.getEdges(cell);
if (edges != null)
{
for (var i=0; i<edges.length; i++)
{
var edgeState = graph.getView().getState(edges[i]);
if (edgeState != null)
{
mxUtils.setOpacity(edgeState.shape.node, opacity);
}
}
}
},
/**
* Function: fadeOut
*
* Asynchronous fade-out operation.
*/
fadeOut: function(node, from, remove, step, delay, isEnabled)
{
step = step || 40;
delay = delay || 30;
var opacity = from || 100;
mxUtils.setOpacity(node, opacity);
if (isEnabled || isEnabled == null)
{
var f = function()
{
opacity = Math.max(opacity-step, 0);
mxUtils.setOpacity(node, opacity);
if (opacity > 0)
{
window.setTimeout(f, delay);
}
else
{
node.style.visibility = 'hidden';
if (remove && node.parentNode)
{
node.parentNode.removeChild(node);
}
}
};
window.setTimeout(f, delay);
}
else
{
node.style.visibility = 'hidden';
if (remove && node.parentNode)
{
node.parentNode.removeChild(node);
}
}
}
};
/**
* 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))
{
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.importNode(doc, node.childNodes[i], allChildren));
}
}
}
return newNode;
break;
}
case 3: /* text */
case 4: /* cdata-section */
case 8: /* comment */
{
return doc.createTextNode(node.value);
break;
}
};
}
else
{
return doc.importNode(node, allChildren);
}
},
/**
* 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 (window.DOMParser)
{
return function(xml)
{
var parser = new DOMParser();
return parser.parseFromString(xml, 'text/xml');
};
}
else // IE<=9
{
return function(xml)
{
var result = mxUtils.createXmlDocument();
result.async = false;
// Workaround for parsing errors with SVG DTD
result.validateOnParse = false;
result.resolveExternals = false;
result.loadXML(xml);
return result;
};
}
}(),
/**
* 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: 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)
{
var value = mxUtils.trim(mxUtils.getTextContent(node));
if (value.length > 0)
{
result.push(indent + mxUtils.htmlEntities(value) + '\n');
}
}
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 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 = String(s || '');
s = s.replace(/&/g,'&amp;'); // 38 26
s = s.replace(/"/g,'&quot;'); // 34 22
s = s.replace(/\'/g,'&#39;'); // 39 27
s = s.replace(/</g,'&lt;'); // 60 3C
s = s.replace(/>/g,'&gt;'); // 62 3E
if (newline == null || newline)
{
s = s.replace(/\n/g, '&#xa;');
}
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 &#xa; if
* no linefeed is defined.
*
* Parameters:
*
* node - DOM node to return the XML for.
* linefeed - Optional string that linefeeds are converted into. Default is
* &#xa;
*/
getXml: function(node, linefeed)
{
var xml = '';
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 || '&#xa;';
xml = xml.replace(/\n/g, linefeed);
return xml;
},
/**
* 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.
*/
get: function(url, onload, onerror, binary, timeout, ontimeout)
{
var req = new mxXmlRequest(url, null, 'GET');
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] == b[i] || (a[i] != null && !a[i].equals(b[i])))
{
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 * 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,
mxUtils.getValue(edge.style, (source) ? mxConstants.STYLE_SOURCE_PORT_CONSTRAINT :
mxConstants.STYLE_TARGET_PORT_CONSTRAINT, null));
if (value == null)
{
return defaultValue;
}
else
{
var directions = value.toString();
var returnValue = mxConstants.DIRECTION_MASK_NONE;
var constraintRotationEnabled = mxUtils.getValue(terminal.style, mxConstants.STYLE_PORT_CONSTRAINT_ROTATION, 0);
var rotation = 0;
if (constraintRotationEnabled == 1)
{
rotation = mxUtils.getValue(terminal.style, mxConstants.STYLE_ROTATION, 0);
}
var quad = 0;
if (rotation > 45)
{
quad = 1;
if (rotation >= 135)
{
quad = 2;
}
}
else if (rotation < -45)
{
quad = 3;
if (rotation <= -135)
{
quad = 2;
}
}
if (directions.indexOf(mxConstants.DIRECTION_NORTH) >= 0)
{
switch (quad)
{
case 0:
returnValue |= mxConstants.DIRECTION_MASK_NORTH;
break;
case 1:
returnValue |= mxConstants.DIRECTION_MASK_EAST;
break;
case 2:
returnValue |= mxConstants.DIRECTION_MASK_SOUTH;
break;
case 3:
returnValue |= mxConstants.DIRECTION_MASK_WEST;
break;
}
}
if (directions.indexOf(mxConstants.DIRECTION_WEST) >= 0)
{
switch (quad)
{
case 0:
returnValue |= mxConstants.DIRECTION_MASK_WEST;
break;
case 1:
returnValue |= mxConstants.DIRECTION_MASK_NORTH;
break;
case 2:
returnValue |= mxConstants.DIRECTION_MASK_EAST;
break;
case 3:
returnValue |= mxConstants.DIRECTION_MASK_SOUTH;
break;
}
}
if (directions.indexOf(mxConstants.DIRECTION_SOUTH) >= 0)
{
switch (quad)
{
case 0:
returnValue |= mxConstants.DIRECTION_MASK_SOUTH;
break;
case 1:
returnValue |= mxConstants.DIRECTION_MASK_WEST;
break;
case 2:
returnValue |= mxConstants.DIRECTION_MASK_NORTH;
break;
case 3:
returnValue |= mxConstants.DIRECTION_MASK_EAST;
break;
}
}
if (directions.indexOf(mxConstants.DIRECTION_EAST) >= 0)
{
switch (quad)
{
case 0:
returnValue |= mxConstants.DIRECTION_MASK_EAST;
break;
case 1:
returnValue |= mxConstants.DIRECTION_MASK_SOUTH;
break;
case 2:
returnValue |= mxConstants.DIRECTION_MASK_WEST;
break;
case 3:
returnValue |= mxConstants.DIRECTION_MASK_NORTH;
break;
}
}
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: getDirectedBounds
*
* Adds the given margins to the given rectangle and rotates and flips the
* rectangle according to the respective styles in style.
*/
getDirectedBounds: function (rect, m, style, flipH, flipV)
{
var d = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST);
flipH = (flipH != null) ? flipH : mxUtils.getValue(style, mxConstants.STYLE_FLIPH, false);
flipV = (flipV != null) ? flipV : mxUtils.getValue(style, mxConstants.STYLE_FLIPV, false);
m.x = Math.round(Math.max(0, Math.min(rect.width, m.x)));
m.y = Math.round(Math.max(0, Math.min(rect.height, m.y)));
m.width = Math.round(Math.max(0, Math.min(rect.width, m.width)));
m.height = Math.round(Math.max(0, Math.min(rect.height, m.height)));
if ((flipV && (d == mxConstants.DIRECTION_SOUTH || d == mxConstants.DIRECTION_NORTH)) ||
(flipH && (d == mxConstants.DIRECTION_EAST || d == mxConstants.DIRECTION_WEST)))
{
var tmp = m.x;
m.x = m.width;
m.width = tmp;
}
if ((flipH && (d == mxConstants.DIRECTION_SOUTH || d == mxConstants.DIRECTION_NORTH)) ||
(flipV && (d == mxConstants.DIRECTION_EAST || d == mxConstants.DIRECTION_WEST)))
{
var tmp = m.y;
m.y = m.height;
m.height = tmp;
}
var m2 = mxRectangle.fromRectangle(m);
if (d == mxConstants.DIRECTION_SOUTH)
{
m2.y = m.x;
m2.x = m.height;
m2.width = m.y;
m2.height = m.width;
}
else if (d == mxConstants.DIRECTION_WEST)
{
m2.y = m.height;
m2.x = m.width;
m2.width = m.x;
m2.height = m.y;
}
else if (d == mxConstants.DIRECTION_NORTH)
{
m2.y = m.width;
m2.x = m.y;
m2.width = m.height;
m2.height = m.x;
}
return new mxRectangle(rect.x + m2.x, rect.y + m2.y, rect.width - m2.width - m2.x, rect.height - m2.height - m2.y);
},
/**
* Function: getPerimeterPoint
*
* Returns the intersection between the polygon defined by the array of
* points and the line between center and point.
*/
getPerimeterPoint: function (pts, center, point)
{
var min = null;
for (var i = 0; i < pts.length - 1; i++)
{
var pt = mxUtils.intersection(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y,
center.x, center.y, point.x, point.y);
if (pt != null)
{
var dx = point.x - pt.x;
var dy = point.y - pt.y;
var ip = {p: pt, distSq: dy * dy + dx * dx};
if (ip != null && (min == null || min.distSq > ip.distSq))
{
min = ip;
}
}
}
return (min != null) ? min.p : null;
},
/**
* 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);
var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0);
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var cx = new mxPoint(state.getCenterX(), state.getCenterY());
var pt = mxUtils.getRotatedPoint(new mxPoint(x, y), cos, sin, cx);
x = pt.x;
y = pt.y;
}
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;
// Ignores document scroll origin for fixed elements
var fixed = false;
var node = container;
var b = document.body;
var d = document.documentElement;
while (node != null && node != b && node != d && !fixed)
{
var style = mxUtils.getCurrentStyle(node);
if (style != null)
{
fixed = fixed || style.position == 'fixed';
}
node = node.parentNode;
}
if (!scrollOffset && !fixed)
{
var offset = mxUtils.getDocumentScrollOrigin(container.ownerDocument);
offsetLeft += offset.x;
offsetTop += offset.y;
}
var r = container.getBoundingClientRect();
if (r != null)
{
offsetLeft += r.left;
offsetTop += r.top;
}
return new mxPoint(offsetLeft, offsetTop);
},
/**
* Function: getDocumentScrollOrigin
*
* Returns the scroll origin of the given document or the current document
* if no document is given.
*/
getDocumentScrollOrigin: function(doc)
{
if (mxClient.IS_QUIRKS)
{
return new mxPoint(doc.body.scrollLeft, doc.body.scrollTop);
}
else
{
var wnd = doc.defaultView || doc.parentWindow;
var x = (wnd != null && window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
var y = (wnd != null && window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
return new mxPoint(x, y);
}
},
/**
* Function: getScrollOrigin
*
* Returns the top, left corner of the viewrect as an <mxPoint>.
*
* Parameters:
*
* node - DOM node whose scroll origin should be returned.
* includeAncestors - Whether the scroll origin of the ancestors should be
* included. Default is false.
* includeDocument - Whether the scroll origin of the document should be
* included. Default is true.
*/
getScrollOrigin: function(node, includeAncestors, includeDocument)
{
includeAncestors = (includeAncestors != null) ? includeAncestors : false;
includeDocument = (includeDocument != null) ? includeDocument : true;
var doc = (node != null) ? node.ownerDocument : document;
var b = doc.body;
var d = doc.documentElement;
var result = new mxPoint();
var fixed = false;
while (node != null && node != b && node != d)
{
if (!isNaN(node.scrollLeft) && !isNaN(node.scrollTop))
{
result.x += node.scrollLeft;
result.y += node.scrollTop;
}
var style = mxUtils.getCurrentStyle(node);
if (style != null)
{
fixed = fixed || style.position == 'fixed';
}
node = (includeAncestors) ? node.parentNode : null;
}
if (!fixed && includeDocument)
{
var origin = mxUtils.getDocumentScrollOrigin(doc);
result.x += origin.x;
result.y += origin.y;
}
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.
*
* (code)
* var pt = mxUtils.convertPoint(graph.container,
* mxEvent.getClientX(evt), mxEvent.getClientY(evt));
* (end)
*
* 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, false);
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, this 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 != null) ? str.replace(new RegExp("^[" + chars + "]+", "g"), "") : null;
},
/**
* Function: rtrim
*
* Strips all whitespaces from the end of the string. Without the second
* parameter, this 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 != null) ? str.replace(new RegExp("[" + chars + "]+$", "g"), "") : null;
},
/**
* 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:
*
* n - String representing the possibly numeric value.
*/
isNumeric: function(n)
{
return !isNaN(parseFloat(n)) && isFinite(n) && (typeof(n) != 'string' || n.toLowerCase().indexOf('0x') < 0);
},
/**
* Function: isInteger
*
* Returns true if the given value is an valid integer number.
*
* Parameters:
*
* n - String representing the possibly numeric value.
*/
isInteger: function(n)
{
return String(parseInt(n)) === String(n);
},
/**
* 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 x = x0 + ua * (x1 - x0);
var y = y0 + ua * (y1 - y0);
return new mxPoint(x, y);
}
// No intersection
return null;
},
/**
* Function: ptSegDistSq
*
* Returns the square distance between a segment and a point. To get the
* distance between a point and a line (with infinite length) use
* <mxUtils.ptLineDist>.
*
* 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: ptLineDist
*
* Returns the distance between a line defined by two points and a point.
* To get the distance between a point and a segment (with a specific
* length) use <mxUtils.ptSeqDistSq>.
*
* Parameters:
*
* x1 - X-coordinate of point 1 of the line.
* y1 - Y-coordinate of point 1 of the line.
* x2 - X-coordinate of point 1 of the line.
* y2 - Y-coordinate of point 1 of the line.
* px - X-coordinate of the point.
* py - Y-coordinate of the point.
*/
ptLineDist: function(x1, y1, x2, y2, px, py)
{
return Math.abs((y2 - y1) * px - (x2 - x1) * py + x2 * y1 - y2 * x1) /
Math.sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1));
},
/**
* 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 = '';
}
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 = '';
}
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
* quirks 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(mxClient.VML_PREFIX + ':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
{
if (style.substring(0, key.length + 1) == key + '=')
{
var next = style.indexOf(';');
if (isValue)
{
style = key + '=' + value + ((next < 0) ? ';' : style.substring(next));
}
else
{
style = (next < 0 || next == style.length - 1) ? '' : style.substring(next + 1);
}
}
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 next = style.indexOf(';', index + 1);
if (isValue)
{
style = style.substring(0, index + 1) + key + '=' + value + ((next < 0) ? ';' : style.substring(next));
}
else
{
style = style.substring(0, index) + ((next < 0) ? ';' : style.substring(next));
}
}
}
}
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: getAlignmentAsPoint
*
* Returns an <mxPoint> that represents the horizontal and vertical alignment
* for numeric computations. X is -0.5 for center, -1 for right and 0 for
* left alignment. Y is -0.5 for middle, -1 for bottom and 0 for top
* alignment. Default values for missing arguments is top, left.
*/
getAlignmentAsPoint: function(align, valign)
{
var dx = 0;
var dy = 0;
// Horizontal alignment
if (align == mxConstants.ALIGN_CENTER)
{
dx = -0.5;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
dx = -1;
}
// Vertical alignment
if (valign == mxConstants.ALIGN_MIDDLE)
{
dy = -0.5;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
dy = -1;
}
return new mxPoint(dx, dy);
},
/**
* 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. The caller is responsible
* for sanitizing the HTML markup.
*
* 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>.
* textWidth - Optional width for text wrapping.
*/
getSizeForString: function(text, fontSize, fontFamily, textWidth)
{
fontSize = (fontSize != null) ? fontSize : mxConstants.DEFAULT_FONTSIZE;
fontFamily = (fontFamily != null) ? fontFamily : mxConstants.DEFAULT_FONTFAMILY;
var div = document.createElement('div');
// Sets the font size and family
div.style.fontFamily = fontFamily;
div.style.fontSize = Math.round(fontSize) + 'px';
div.style.lineHeight = Math.round(fontSize * mxConstants.LINE_HEIGHT) + 'px';
// Disables block layout and outside wrapping and hides the div
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
div.style.zoom = '1';
if (textWidth != null)
{
div.style.width = textWidth + 'px';
div.style.whiteSpace = 'normal';
}
else
{
div.style.whiteSpace = 'nowrap';
}
// 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.
* w - Optional width of the graph view.
* h - Optional height of the graph view.
*/
show: function(graph, doc, x0, y0, w, h)
{
x0 = (x0 != null) ? x0 : 0;
y0 = (y0 != null) ? y0 : 0;
if (doc == null)
{
var wnd = window.open();
doc = wnd.document;
}
else
{
doc.open();
}
// Workaround for missing print output in IE9 standards
if (document.documentMode == 9)
{
doc.writeln('<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=9"><![endif]-->');
}
var bounds = graph.getGraphBounds();
var dx = Math.ceil(x0 - bounds.x);
var dy = Math.ceil(y0 - bounds.y);
if (w == null)
{
w = Math.ceil(bounds.width + x0) + Math.ceil(Math.ceil(bounds.x) - bounds.x);
}
if (h == null)
{
h = Math.ceil(bounds.height + y0) + Math.ceil(Math.ceil(bounds.y) - bounds.y);
}
// 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 || document.documentMode == 11)
{
var 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></head><body style="margin:0px;">';
// Copies the contents of the graph container
html += '<div style="position:absolute;overflow:hidden;width:' + w + 'px;height:' + h + 'px;"><div style="position:relative;left:' + dx + 'px;top:' + dy + 'px;">';
html += graph.container.innerHTML;
html += '</div></div></body><html>';
doc.writeln(html);
doc.close();
}
else
{
doc.writeln('<html><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><body style="margin:0px;"></body></html>');
doc.close();
var outer = doc.createElement('div');
outer.position = 'absolute';
outer.overflow = 'hidden';
outer.style.width = w + 'px';
outer.style.height = h + 'px';
// Required for HTML labels if foreignObjects are disabled
var div = doc.createElement('div');
div.style.position = 'absolute';
div.style.left = dx + 'px';
div.style.top = dy + 'px';
var node = graph.container.firstChild;
var svg = null;
while (node != null)
{
var clone = node.cloneNode(true);
if (node == graph.view.drawPane.ownerSVGElement)
{
outer.appendChild(clone);
svg = clone;
}
else
{
div.appendChild(clone);
}
node = node.nextSibling;
}
doc.body.appendChild(outer);
if (div.firstChild != null)
{
doc.body.appendChild(div);
}
if (svg != null)
{
svg.style.minWidth = '';
svg.style.minHeight = '';
svg.firstChild.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
}
}
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();
var bounds = graph.getGraphBounds();
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, '&nbsp;');
div.appendChild(pre);
var w = document.body.clientWidth;
var h = Math.max(document.body.clientHeight || 0, 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, '&nbsp;');
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 != null) ? 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')); // &nbsp;
div.appendChild(document.createTextNode('\u00a0')); // &nbsp;
div.appendChild(document.createTextNode('\u00a0')); // &nbsp;
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;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxConstants =
{
/**
* Class: mxConstants
*
* Defines various global constants.
*
* Variable: DEFAULT_HOTSPOT
*
* Defines the portion of the cell which is to be used as a connectable
* region. Default is 0.3. Possible values are 0 < x <= 1.
*/
DEFAULT_HOTSPOT: 0.3,
/**
* Variable: MIN_HOTSPOT_SIZE
*
* Defines the minimum size in pixels of the portion of the cell which is
* to be used as a connectable region. Default is 8.
*/
MIN_HOTSPOT_SIZE: 8,
/**
* Variable: MAX_HOTSPOT_SIZE
*
* Defines the maximum size in pixels of the portion of the cell which is
* to be used as a connectable region. Use 0 for no maximum. Default is 0.
*/
MAX_HOTSPOT_SIZE: 0,
/**
* Variable: RENDERING_HINT_EXACT
*
* Defines the exact rendering hint.
*/
RENDERING_HINT_EXACT: 'exact',
/**
* Variable: RENDERING_HINT_FASTER
*
* Defines the faster rendering hint.
*/
RENDERING_HINT_FASTER: 'faster',
/**
* Variable: RENDERING_HINT_FASTEST
*
* Defines the fastest rendering hint.
*/
RENDERING_HINT_FASTEST: 'fastest',
/**
* Variable: DIALECT_SVG
*
* Defines the SVG display dialect name.
*/
DIALECT_SVG: 'svg',
/**
* Variable: DIALECT_VML
*
* Defines the VML display dialect name.
*/
DIALECT_VML: 'vml',
/**
* Variable: DIALECT_MIXEDHTML
*
* Defines the mixed HTML display dialect name.
*/
DIALECT_MIXEDHTML: 'mixedHtml',
/**
* Variable: DIALECT_PREFERHTML
*
* Defines the preferred HTML display dialect name.
*/
DIALECT_PREFERHTML: 'preferHtml',
/**
* Variable: DIALECT_STRICTHTML
*
* Defines the strict HTML display dialect.
*/
DIALECT_STRICTHTML: 'strictHtml',
/**
* Variable: NS_SVG
*
* Defines the SVG namespace.
*/
NS_SVG: 'http://www.w3.org/2000/svg',
/**
* Variable: NS_XHTML
*
* Defines the XHTML namespace.
*/
NS_XHTML: 'http://www.w3.org/1999/xhtml',
/**
* Variable: NS_XLINK
*
* Defines the XLink namespace.
*/
NS_XLINK: 'http://www.w3.org/1999/xlink',
/**
* Variable: SHADOWCOLOR
*
* Defines the color to be used to draw shadows in shapes and windows.
* Default is gray.
*/
SHADOWCOLOR: 'gray',
/**
* Variable: VML_SHADOWCOLOR
*
* Used for shadow color in filters where transparency is not supported
* (Microsoft Internet Explorer). Default is gray.
*/
VML_SHADOWCOLOR: 'gray',
/**
* Variable: SHADOW_OFFSET_X
*
* Specifies the x-offset of the shadow. Default is 2.
*/
SHADOW_OFFSET_X: 2,
/**
* Variable: SHADOW_OFFSET_Y
*
* Specifies the y-offset of the shadow. Default is 3.
*/
SHADOW_OFFSET_Y: 3,
/**
* Variable: SHADOW_OPACITY
*
* Defines the opacity for shadows. Default is 1.
*/
SHADOW_OPACITY: 1,
/**
* Variable: NODETYPE_ELEMENT
*
* DOM node of type ELEMENT.
*/
NODETYPE_ELEMENT: 1,
/**
* Variable: NODETYPE_ATTRIBUTE
*
* DOM node of type ATTRIBUTE.
*/
NODETYPE_ATTRIBUTE: 2,
/**
* Variable: NODETYPE_TEXT
*
* DOM node of type TEXT.
*/
NODETYPE_TEXT: 3,
/**
* Variable: NODETYPE_CDATA
*
* DOM node of type CDATA.
*/
NODETYPE_CDATA: 4,
/**
* Variable: NODETYPE_ENTITY_REFERENCE
*
* DOM node of type ENTITY_REFERENCE.
*/
NODETYPE_ENTITY_REFERENCE: 5,
/**
* Variable: NODETYPE_ENTITY
*
* DOM node of type ENTITY.
*/
NODETYPE_ENTITY: 6,
/**
* Variable: NODETYPE_PROCESSING_INSTRUCTION
*
* DOM node of type PROCESSING_INSTRUCTION.
*/
NODETYPE_PROCESSING_INSTRUCTION: 7,
/**
* Variable: NODETYPE_COMMENT
*
* DOM node of type COMMENT.
*/
NODETYPE_COMMENT: 8,
/**
* Variable: NODETYPE_DOCUMENT
*
* DOM node of type DOCUMENT.
*/
NODETYPE_DOCUMENT: 9,
/**
* Variable: NODETYPE_DOCUMENTTYPE
*
* DOM node of type DOCUMENTTYPE.
*/
NODETYPE_DOCUMENTTYPE: 10,
/**
* Variable: NODETYPE_DOCUMENT_FRAGMENT
*
* DOM node of type DOCUMENT_FRAGMENT.
*/
NODETYPE_DOCUMENT_FRAGMENT: 11,
/**
* Variable: NODETYPE_NOTATION
*
* DOM node of type NOTATION.
*/
NODETYPE_NOTATION: 12,
/**
* Variable: TOOLTIP_VERTICAL_OFFSET
*
* Defines the vertical offset for the tooltip.
* Default is 16.
*/
TOOLTIP_VERTICAL_OFFSET: 16,
/**
* Variable: DEFAULT_VALID_COLOR
*
* Specifies the default valid color. Default is #0000FF.
*/
DEFAULT_VALID_COLOR: '#00FF00',
/**
* Variable: DEFAULT_INVALID_COLOR
*
* Specifies the default invalid color. Default is #FF0000.
*/
DEFAULT_INVALID_COLOR: '#FF0000',
/**
* Variable: OUTLINE_HIGHLIGHT_COLOR
*
* Specifies the default highlight color for shape outlines.
* Default is #0000FF. This is used in <mxEdgeHandler>.
*/
OUTLINE_HIGHLIGHT_COLOR: '#00FF00',
/**
* Variable: OUTLINE_HIGHLIGHT_COLOR
*
* Defines the strokewidth to be used for shape outlines.
* Default is 5. This is used in <mxEdgeHandler>.
*/
OUTLINE_HIGHLIGHT_STROKEWIDTH: 5,
/**
* Variable: HIGHLIGHT_STROKEWIDTH
*
* Defines the strokewidth to be used for the highlights.
* Default is 3.
*/
HIGHLIGHT_STROKEWIDTH: 3,
/**
* Variable: CONSTRAINT_HIGHLIGHT_SIZE
*
* Size of the constraint highlight (in px). Default is 2.
*/
HIGHLIGHT_SIZE: 2,
/**
* Variable: HIGHLIGHT_OPACITY
*
* Opacity (in %) used for the highlights (including outline).
* Default is 100.
*/
HIGHLIGHT_OPACITY: 100,
/**
* Variable: CURSOR_MOVABLE_VERTEX
*
* Defines the cursor for a movable vertex. Default is 'move'.
*/
CURSOR_MOVABLE_VERTEX: 'move',
/**
* Variable: CURSOR_MOVABLE_EDGE
*
* Defines the cursor for a movable edge. Default is 'move'.
*/
CURSOR_MOVABLE_EDGE: 'move',
/**
* Variable: CURSOR_LABEL_HANDLE
*
* Defines the cursor for a movable label. Default is 'default'.
*/
CURSOR_LABEL_HANDLE: 'default',
/**
* Variable: CURSOR_TERMINAL_HANDLE
*
* Defines the cursor for a terminal handle. Default is 'pointer'.
*/
CURSOR_TERMINAL_HANDLE: 'pointer',
/**
* Variable: CURSOR_BEND_HANDLE
*
* Defines the cursor for a movable bend. Default is 'crosshair'.
*/
CURSOR_BEND_HANDLE: 'crosshair',
/**
* Variable: CURSOR_VIRTUAL_BEND_HANDLE
*
* Defines the cursor for a movable bend. Default is 'crosshair'.
*/
CURSOR_VIRTUAL_BEND_HANDLE: 'crosshair',
/**
* Variable: CURSOR_CONNECT
*
* Defines the cursor for a connectable state. Default is 'pointer'.
*/
CURSOR_CONNECT: 'pointer',
/**
* Variable: HIGHLIGHT_COLOR
*
* Defines the color to be used for the cell highlighting.
* Use 'none' for no color. Default is #00FF00.
*/
HIGHLIGHT_COLOR: '#00FF00',
/**
* Variable: TARGET_HIGHLIGHT_COLOR
*
* Defines the color to be used for highlighting a target cell for a new
* or changed connection. Note that this may be either a source or
* target terminal in the graph. Use 'none' for no color.
* Default is #0000FF.
*/
CONNECT_TARGET_COLOR: '#0000FF',
/**
* Variable: INVALID_CONNECT_TARGET_COLOR
*
* Defines the color to be used for highlighting a invalid target cells
* for a new or changed connections. Note that this may be either a source
* or target terminal in the graph. Use 'none' for no color. Default is
* #FF0000.
*/
INVALID_CONNECT_TARGET_COLOR: '#FF0000',
/**
* Variable: DROP_TARGET_COLOR
*
* Defines the color to be used for the highlighting target parent cells
* (for drag and drop). Use 'none' for no color. Default is #0000FF.
*/
DROP_TARGET_COLOR: '#0000FF',
/**
* Variable: VALID_COLOR
*
* Defines the color to be used for the coloring valid connection
* previews. Use 'none' for no color. Default is #FF0000.
*/
VALID_COLOR: '#00FF00',
/**
* Variable: INVALID_COLOR
*
* Defines the color to be used for the coloring invalid connection
* previews. Use 'none' for no color. Default is #FF0000.
*/
INVALID_COLOR: '#FF0000',
/**
* Variable: EDGE_SELECTION_COLOR
*
* Defines the color to be used for the selection border of edges. Use
* 'none' for no color. Default is #00FF00.
*/
EDGE_SELECTION_COLOR: '#00FF00',
/**
* Variable: VERTEX_SELECTION_COLOR
*
* Defines the color to be used for the selection border of vertices. Use
* 'none' for no color. Default is #00FF00.
*/
VERTEX_SELECTION_COLOR: '#00FF00',
/**
* Variable: VERTEX_SELECTION_STROKEWIDTH
*
* Defines the strokewidth to be used for vertex selections.
* Default is 1.
*/
VERTEX_SELECTION_STROKEWIDTH: 1,
/**
* Variable: EDGE_SELECTION_STROKEWIDTH
*
* Defines the strokewidth to be used for edge selections.
* Default is 1.
*/
EDGE_SELECTION_STROKEWIDTH: 1,
/**
* Variable: SELECTION_DASHED
*
* Defines the dashed state to be used for the vertex selection
* border. Default is true.
*/
VERTEX_SELECTION_DASHED: true,
/**
* Variable: SELECTION_DASHED
*
* Defines the dashed state to be used for the edge selection
* border. Default is true.
*/
EDGE_SELECTION_DASHED: true,
/**
* Variable: GUIDE_COLOR
*
* Defines the color to be used for the guidelines in mxGraphHandler.
* Default is #FF0000.
*/
GUIDE_COLOR: '#FF0000',
/**
* Variable: GUIDE_STROKEWIDTH
*
* Defines the strokewidth to be used for the guidelines in mxGraphHandler.
* Default is 1.
*/
GUIDE_STROKEWIDTH: 1,
/**
* Variable: OUTLINE_COLOR
*
* Defines the color to be used for the outline rectangle
* border. Use 'none' for no color. Default is #0099FF.
*/
OUTLINE_COLOR: '#0099FF',
/**
* Variable: OUTLINE_STROKEWIDTH
*
* Defines the strokewidth to be used for the outline rectangle
* stroke width. Default is 3.
*/
OUTLINE_STROKEWIDTH: (mxClient.IS_IE) ? 2 : 3,
/**
* Variable: HANDLE_SIZE
*
* Defines the default size for handles. Default is 6.
*/
HANDLE_SIZE: 6,
/**
* Variable: LABEL_HANDLE_SIZE
*
* Defines the default size for label handles. Default is 4.
*/
LABEL_HANDLE_SIZE: 4,
/**
* Variable: HANDLE_FILLCOLOR
*
* Defines the color to be used for the handle fill color. Use 'none' for
* no color. Default is #00FF00 (green).
*/
HANDLE_FILLCOLOR: '#00FF00',
/**
* Variable: HANDLE_STROKECOLOR
*
* Defines the color to be used for the handle stroke color. Use 'none' for
* no color. Default is black.
*/
HANDLE_STROKECOLOR: 'black',
/**
* Variable: LABEL_HANDLE_FILLCOLOR
*
* Defines the color to be used for the label handle fill color. Use 'none'
* for no color. Default is yellow.
*/
LABEL_HANDLE_FILLCOLOR: 'yellow',
/**
* Variable: CONNECT_HANDLE_FILLCOLOR
*
* Defines the color to be used for the connect handle fill color. Use
* 'none' for no color. Default is #0000FF (blue).
*/
CONNECT_HANDLE_FILLCOLOR: '#0000FF',
/**
* Variable: LOCKED_HANDLE_FILLCOLOR
*
* Defines the color to be used for the locked handle fill color. Use
* 'none' for no color. Default is #FF0000 (red).
*/
LOCKED_HANDLE_FILLCOLOR: '#FF0000',
/**
* Variable: OUTLINE_HANDLE_FILLCOLOR
*
* Defines the color to be used for the outline sizer fill color. Use
* 'none' for no color. Default is #00FFFF.
*/
OUTLINE_HANDLE_FILLCOLOR: '#00FFFF',
/**
* Variable: OUTLINE_HANDLE_STROKECOLOR
*
* Defines the color to be used for the outline sizer stroke color. Use
* 'none' for no color. Default is #0033FF.
*/
OUTLINE_HANDLE_STROKECOLOR: '#0033FF',
/**
* Variable: DEFAULT_FONTFAMILY
*
* Defines the default family for all fonts. Default is Arial,Helvetica.
*/
DEFAULT_FONTFAMILY: 'Arial,Helvetica',
/**
* Variable: DEFAULT_FONTSIZE
*
* Defines the default size (in px). Default is 11.
*/
DEFAULT_FONTSIZE: 11,
/**
* Variable: DEFAULT_TEXT_DIRECTION
*
* Defines the default value for the <STYLE_TEXT_DIRECTION> if no value is
* defined for it in the style. Default value is an empty string which means
* the default system setting is used and no direction is set.
*/
DEFAULT_TEXT_DIRECTION: '',
/**
* Variable: LINE_HEIGHT
*
* Defines the default line height for text labels. Default is 1.2.
*/
LINE_HEIGHT: 1.2,
/**
* Variable: WORD_WRAP
*
* Defines the CSS value for the word-wrap property. Default is "normal".
* Change this to "break-word" to allow long words to be able to be broken
* and wrap onto the next line.
*/
WORD_WRAP: 'normal',
/**
* Variable: ABSOLUTE_LINE_HEIGHT
*
* Specifies if absolute line heights should be used (px) in CSS. Default
* is false. Set this to true for backwards compatibility.
*/
ABSOLUTE_LINE_HEIGHT: false,
/**
* Variable: DEFAULT_FONTSTYLE
*
* Defines the default style for all fonts. Default is 0. This can be set
* to any combination of font styles as follows.
*
* (code)
* mxConstants.DEFAULT_FONTSTYLE = mxConstants.FONT_BOLD | mxConstants.FONT_ITALIC;
* (end)
*/
DEFAULT_FONTSTYLE: 0,
/**
* Variable: DEFAULT_STARTSIZE
*
* Defines the default start size for swimlanes. Default is 40.
*/
DEFAULT_STARTSIZE: 40,
/**
* Variable: DEFAULT_MARKERSIZE
*
* Defines the default size for all markers. Default is 6.
*/
DEFAULT_MARKERSIZE: 6,
/**
* Variable: DEFAULT_IMAGESIZE
*
* Defines the default width and height for images used in the
* label shape. Default is 24.
*/
DEFAULT_IMAGESIZE: 24,
/**
* Variable: ENTITY_SEGMENT
*
* Defines the length of the horizontal segment of an Entity Relation.
* This can be overridden using <mxConstants.STYLE_SEGMENT> style.
* Default is 30.
*/
ENTITY_SEGMENT: 30,
/**
* Variable: RECTANGLE_ROUNDING_FACTOR
*
* Defines the rounding factor for rounded rectangles in percent between
* 0 and 1. Values should be smaller than 0.5. Default is 0.15.
*/
RECTANGLE_ROUNDING_FACTOR: 0.15,
/**
* Variable: LINE_ARCSIZE
*
* Defines the size of the arcs for rounded edges. Default is 20.
*/
LINE_ARCSIZE: 20,
/**
* Variable: ARROW_SPACING
*
* Defines the spacing between the arrow shape and its terminals. Default is 0.
*/
ARROW_SPACING: 0,
/**
* Variable: ARROW_WIDTH
*
* Defines the width of the arrow shape. Default is 30.
*/
ARROW_WIDTH: 30,
/**
* Variable: ARROW_SIZE
*
* Defines the size of the arrowhead in the arrow shape. Default is 30.
*/
ARROW_SIZE: 30,
/**
* Variable: PAGE_FORMAT_A4_PORTRAIT
*
* Defines the rectangle for the A4 portrait page format. The dimensions
* of this page format are 826x1169 pixels.
*/
PAGE_FORMAT_A4_PORTRAIT: new mxRectangle(0, 0, 827, 1169),
/**
* Variable: PAGE_FORMAT_A4_PORTRAIT
*
* Defines the rectangle for the A4 portrait page format. The dimensions
* of this page format are 826x1169 pixels.
*/
PAGE_FORMAT_A4_LANDSCAPE: new mxRectangle(0, 0, 1169, 827),
/**
* Variable: PAGE_FORMAT_LETTER_PORTRAIT
*
* Defines the rectangle for the Letter portrait page format. The
* dimensions of this page format are 850x1100 pixels.
*/
PAGE_FORMAT_LETTER_PORTRAIT: new mxRectangle(0, 0, 850, 1100),
/**
* Variable: PAGE_FORMAT_LETTER_PORTRAIT
*
* Defines the rectangle for the Letter portrait page format. The dimensions
* of this page format are 850x1100 pixels.
*/
PAGE_FORMAT_LETTER_LANDSCAPE: new mxRectangle(0, 0, 1100, 850),
/**
* Variable: NONE
*
* Defines the value for none. Default is "none".
*/
NONE: 'none',
/**
* Variable: STYLE_PERIMETER
*
* Defines the key for the perimeter style. This is a function that defines
* the perimeter around a particular shape. Possible values are the
* functions defined in <mxPerimeter>. Alternatively, the constants in this
* class that start with "PERIMETER_" may be used to access
* perimeter styles in <mxStyleRegistry>. Value is "perimeter".
*/
STYLE_PERIMETER: 'perimeter',
/**
* Variable: STYLE_SOURCE_PORT
*
* Defines the ID of the cell that should be used for computing the
* perimeter point of the source for an edge. This allows for graphically
* connecting to a cell while keeping the actual terminal of the edge.
* Value is "sourcePort".
*/
STYLE_SOURCE_PORT: 'sourcePort',
/**
* Variable: STYLE_TARGET_PORT
*
* Defines the ID of the cell that should be used for computing the
* perimeter point of the target for an edge. This allows for graphically
* connecting to a cell while keeping the actual terminal of the edge.
* Value is "targetPort".
*/
STYLE_TARGET_PORT: 'targetPort',
/**
* Variable: STYLE_PORT_CONSTRAINT
*
* Defines the direction(s) that edges are allowed to connect to cells in.
* Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH,
* DIRECTION_EAST" and "DIRECTION_WEST". Value is
* "portConstraint".
*/
STYLE_PORT_CONSTRAINT: 'portConstraint',
/**
* Variable: STYLE_PORT_CONSTRAINT_ROTATION
*
* Define whether port constraint directions are rotated with vertex
* rotation. 0 (default) causes port constraints to remain absolute,
* relative to the graph, 1 causes the constraints to rotate with
* the vertex. Value is "portConstraintRotation".
*/
STYLE_PORT_CONSTRAINT_ROTATION: 'portConstraintRotation',
/**
* Variable: STYLE_SOURCE_PORT_CONSTRAINT
*
* Defines the direction(s) that edges are allowed to connect to sources in.
* Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH, DIRECTION_EAST"
* and "DIRECTION_WEST". Value is "sourcePortConstraint".
*/
STYLE_SOURCE_PORT_CONSTRAINT: 'sourcePortConstraint',
/**
* Variable: STYLE_TARGET_PORT_CONSTRAINT
*
* Defines the direction(s) that edges are allowed to connect to targets in.
* Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH, DIRECTION_EAST"
* and "DIRECTION_WEST". Value is "targetPortConstraint".
*/
STYLE_TARGET_PORT_CONSTRAINT: 'targetPortConstraint',
/**
* Variable: STYLE_OPACITY
*
* Defines the key for the opacity style. The type of the value is
* numeric and the possible range is 0-100. Value is "opacity".
*/
STYLE_OPACITY: 'opacity',
/**
* Variable: STYLE_FILL_OPACITY
*
* Defines the key for the fill opacity style. The type of the value is
* numeric and the possible range is 0-100. Value is "fillOpacity".
*/
STYLE_FILL_OPACITY: 'fillOpacity',
/**
* Variable: STYLE_STROKE_OPACITY
*
* Defines the key for the stroke opacity style. The type of the value is
* numeric and the possible range is 0-100. Value is "strokeOpacity".
*/
STYLE_STROKE_OPACITY: 'strokeOpacity',
/**
* Variable: STYLE_TEXT_OPACITY
*
* Defines the key for the text opacity style. The type of the value is
* numeric and the possible range is 0-100. Value is "textOpacity".
*/
STYLE_TEXT_OPACITY: 'textOpacity',
/**
* Variable: STYLE_TEXT_DIRECTION
*
* Defines the key for the text direction style. Possible values are
* "TEXT_DIRECTION_DEFAULT, TEXT_DIRECTION_AUTO, TEXT_DIRECTION_LTR"
* and "TEXT_DIRECTION_RTL". Value is "textDirection".
* The default value for the style is defined in <DEFAULT_TEXT_DIRECTION>.
* It is used is no value is defined for this key in a given style. This is
* an experimental style that is currently ignored in the backends.
*/
STYLE_TEXT_DIRECTION: 'textDirection',
/**
* Variable: STYLE_OVERFLOW
*
* Defines the key for the overflow style. Possible values are 'visible',
* 'hidden', 'fill' and 'width'. The default value is 'visible'. This value
* specifies how overlapping vertex labels are handled. A value of
* 'visible' will show the complete label. A value of 'hidden' will clip
* the label so that it does not overlap the vertex bounds. A value of
* 'fill' will use the vertex bounds and a value of 'width' will use the
* the vertex width for the label. See <mxGraph.isLabelClipped>. Note that
* the vertical alignment is ignored for overflow fill and for horizontal
* alignment, left should be used to avoid pixel offsets in Internet Explorer
* 11 and earlier or if foreignObjects are disabled. Value is "overflow".
*/
STYLE_OVERFLOW: 'overflow',
/**
* Variable: STYLE_ORTHOGONAL
*
* Defines if the connection points on either end of the edge should be
* computed so that the edge is vertical or horizontal if possible and
* if the point is not at a fixed location. Default is false. This is
* used in <mxGraph.isOrthogonal>, which also returns true if the edgeStyle
* of the edge is an elbow or entity. Value is "orthogonal".
*/
STYLE_ORTHOGONAL: 'orthogonal',
/**
* Variable: STYLE_EXIT_X
*
* Defines the key for the horizontal relative coordinate connection point
* of an edge with its source terminal. Value is "exitX".
*/
STYLE_EXIT_X: 'exitX',
/**
* Variable: STYLE_EXIT_Y
*
* Defines the key for the vertical relative coordinate connection point
* of an edge with its source terminal. Value is "exitY".
*/
STYLE_EXIT_Y: 'exitY',
/**
* Variable: STYLE_EXIT_DX
*
* Defines the key for the horizontal offset of the connection point
* of an edge with its source terminal. Value is "exitDx".
*/
STYLE_EXIT_DX: 'exitDx',
/**
* Variable: STYLE_EXIT_DY
*
* Defines the key for the vertical offset of the connection point
* of an edge with its source terminal. Value is "exitDy".
*/
STYLE_EXIT_DY: 'exitDy',
/**
* Variable: STYLE_EXIT_PERIMETER
*
* Defines if the perimeter should be used to find the exact entry point
* along the perimeter of the source. Possible values are 0 (false) and
* 1 (true). Default is 1 (true). Value is "exitPerimeter".
*/
STYLE_EXIT_PERIMETER: 'exitPerimeter',
/**
* Variable: STYLE_ENTRY_X
*
* Defines the key for the horizontal relative coordinate connection point
* of an edge with its target terminal. Value is "entryX".
*/
STYLE_ENTRY_X: 'entryX',
/**
* Variable: STYLE_ENTRY_Y
*
* Defines the key for the vertical relative coordinate connection point
* of an edge with its target terminal. Value is "entryY".
*/
STYLE_ENTRY_Y: 'entryY',
/**
* Variable: STYLE_ENTRY_DX
*
* Defines the key for the horizontal offset of the connection point
* of an edge with its target terminal. Value is "entryDx".
*/
STYLE_ENTRY_DX: 'entryDx',
/**
* Variable: STYLE_ENTRY_DY
*
* Defines the key for the vertical offset of the connection point
* of an edge with its target terminal. Value is "entryDy".
*/
STYLE_ENTRY_DY: 'entryDy',
/**
* Variable: STYLE_ENTRY_PERIMETER
*
* Defines if the perimeter should be used to find the exact entry point
* along the perimeter of the target. Possible values are 0 (false) and
* 1 (true). Default is 1 (true). Value is "entryPerimeter".
*/
STYLE_ENTRY_PERIMETER: 'entryPerimeter',
/**
* Variable: STYLE_WHITE_SPACE
*
* Defines the key for the white-space style. Possible values are 'nowrap'
* and 'wrap'. The default value is 'nowrap'. This value specifies how
* white-space inside a HTML vertex label should be handled. A value of
* 'nowrap' means the text will never wrap to the next line until a
* linefeed is encountered. A value of 'wrap' means text will wrap when
* necessary. This style is only used for HTML labels.
* See <mxGraph.isWrapping>. Value is "whiteSpace".
*/
STYLE_WHITE_SPACE: 'whiteSpace',
/**
* Variable: STYLE_ROTATION
*
* Defines the key for the rotation style. The type of the value is
* numeric and the possible range is 0-360. Value is "rotation".
*/
STYLE_ROTATION: 'rotation',
/**
* Variable: STYLE_FILLCOLOR
*
* Defines the key for the fill color. Possible values are all HTML color
* names or HEX codes, as well as special keywords such as 'swimlane,
* 'inherit' or 'indicated' to use the color code of a related cell or the
* indicator shape. Value is "fillColor".
*/
STYLE_FILLCOLOR: 'fillColor',
/**
* Variable: STYLE_POINTER_EVENTS
*
* Specifies if pointer events should be fired on transparent backgrounds.
* This style is currently only supported in <mxRectangleShape>. Default
* is true. Value is "pointerEvents". This is typically set to
* false in groups where the transparent part should allow any underlying
* cells to be clickable.
*/
STYLE_POINTER_EVENTS: 'pointerEvents',
/**
* Variable: STYLE_SWIMLANE_FILLCOLOR
*
* Defines the key for the fill color of the swimlane background. Possible
* values are all HTML color names or HEX codes. Default is no background.
* Value is "swimlaneFillColor".
*/
STYLE_SWIMLANE_FILLCOLOR: 'swimlaneFillColor',
/**
* Variable: STYLE_MARGIN
*
* Defines the key for the margin between the ellipses in the double ellipse shape.
* Possible values are all positive numbers. Value is "margin".
*/
STYLE_MARGIN: 'margin',
/**
* Variable: STYLE_GRADIENTCOLOR
*
* Defines the key for the gradient color. Possible values are all HTML color
* names or HEX codes, as well as special keywords such as 'swimlane,
* 'inherit' or 'indicated' to use the color code of a related cell or the
* indicator shape. This is ignored if no fill color is defined. Value is
* "gradientColor".
*/
STYLE_GRADIENTCOLOR: 'gradientColor',
/**
* Variable: STYLE_GRADIENT_DIRECTION
*
* Defines the key for the gradient direction. Possible values are
* <DIRECTION_EAST>, <DIRECTION_WEST>, <DIRECTION_NORTH> and
* <DIRECTION_SOUTH>. Default is <DIRECTION_SOUTH>. Generally, and by
* default in mxGraph, gradient painting is done from the value of
* <STYLE_FILLCOLOR> to the value of <STYLE_GRADIENTCOLOR>. Taking the
* example of <DIRECTION_NORTH>, this means <STYLE_FILLCOLOR> color at the
* bottom of paint pattern and <STYLE_GRADIENTCOLOR> at top, with a
* gradient in-between. Value is "gradientDirection".
*/
STYLE_GRADIENT_DIRECTION: 'gradientDirection',
/**
* Variable: STYLE_STROKECOLOR
*
* Defines the key for the strokeColor style. Possible values are all HTML
* color names or HEX codes, as well as special keywords such as 'swimlane,
* 'inherit', 'indicated' to use the color code of a related cell or the
* indicator shape or 'none' for no color. Value is "strokeColor".
*/
STYLE_STROKECOLOR: 'strokeColor',
/**
* Variable: STYLE_SEPARATORCOLOR
*
* Defines the key for the separatorColor style. Possible values are all
* HTML color names or HEX codes. This style is only used for
* <SHAPE_SWIMLANE> shapes. Value is "separatorColor".
*/
STYLE_SEPARATORCOLOR: 'separatorColor',
/**
* Variable: STYLE_STROKEWIDTH
*
* Defines the key for the strokeWidth style. The type of the value is
* numeric and the possible range is any non-negative value larger or equal
* to 1. The value defines the stroke width in pixels. Note: To hide a
* stroke use strokeColor none. Value is "strokeWidth".
*/
STYLE_STROKEWIDTH: 'strokeWidth',
/**
* Variable: STYLE_ALIGN
*
* Defines the key for the align style. Possible values are <ALIGN_LEFT>,
* <ALIGN_CENTER> and <ALIGN_RIGHT>. This value defines how the lines of
* the label are horizontally aligned. <ALIGN_LEFT> mean label text lines
* are aligned to left of the label bounds, <ALIGN_RIGHT> to the right of
* the label bounds and <ALIGN_CENTER> means the center of the text lines
* are aligned in the center of the label bounds. Note this value doesn't
* affect the positioning of the overall label bounds relative to the
* vertex, to move the label bounds horizontally, use
* <STYLE_LABEL_POSITION>. Value is "align".
*/
STYLE_ALIGN: 'align',
/**
* Variable: STYLE_VERTICAL_ALIGN
*
* Defines the key for the verticalAlign style. Possible values are
* <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM>. This value defines how
* the lines of the label are vertically aligned. <ALIGN_TOP> means the
* topmost label text line is aligned against the top of the label bounds,
* <ALIGN_BOTTOM> means the bottom-most label text line is aligned against
* the bottom of the label bounds and <ALIGN_MIDDLE> means there is equal
* spacing between the topmost text label line and the top of the label
* bounds and the bottom-most text label line and the bottom of the label
* bounds. Note this value doesn't affect the positioning of the overall
* label bounds relative to the vertex, to move the label bounds
* vertically, use <STYLE_VERTICAL_LABEL_POSITION>. Value is "verticalAlign".
*/
STYLE_VERTICAL_ALIGN: 'verticalAlign',
/**
* Variable: STYLE_LABEL_WIDTH
*
* Defines the key for the width of the label if the label position is not
* center. Value is "labelWidth".
*/
STYLE_LABEL_WIDTH: 'labelWidth',
/**
* Variable: STYLE_LABEL_POSITION
*
* Defines the key for the horizontal label position of vertices. Possible
* values are <ALIGN_LEFT>, <ALIGN_CENTER> and <ALIGN_RIGHT>. Default is
* <ALIGN_CENTER>. The label align defines the position of the label
* relative to the cell. <ALIGN_LEFT> means the entire label bounds is
* placed completely just to the left of the vertex, <ALIGN_RIGHT> means
* adjust to the right and <ALIGN_CENTER> means the label bounds are
* vertically aligned with the bounds of the vertex. Note this value
* doesn't affect the positioning of label within the label bounds, to move
* the label horizontally within the label bounds, use <STYLE_ALIGN>.
* Value is "labelPosition".
*/
STYLE_LABEL_POSITION: 'labelPosition',
/**
* Variable: STYLE_VERTICAL_LABEL_POSITION
*
* Defines the key for the vertical label position of vertices. Possible
* values are <ALIGN_TOP>, <ALIGN_BOTTOM> and <ALIGN_MIDDLE>. Default is
* <ALIGN_MIDDLE>. The label align defines the position of the label
* relative to the cell. <ALIGN_TOP> means the entire label bounds is
* placed completely just on the top of the vertex, <ALIGN_BOTTOM> means
* adjust on the bottom and <ALIGN_MIDDLE> means the label bounds are
* horizontally aligned with the bounds of the vertex. Note this value
* doesn't affect the positioning of label within the label bounds, to move
* the label vertically within the label bounds, use
* <STYLE_VERTICAL_ALIGN>. Value is "verticalLabelPosition".
*/
STYLE_VERTICAL_LABEL_POSITION: 'verticalLabelPosition',
/**
* Variable: STYLE_IMAGE_ASPECT
*
* Defines the key for the image aspect style. Possible values are 0 (do
* not preserve aspect) or 1 (keep aspect). This is only used in
* <mxImageShape>. Default is 1. Value is "imageAspect".
*/
STYLE_IMAGE_ASPECT: 'imageAspect',
/**
* Variable: STYLE_IMAGE_ALIGN
*
* Defines the key for the align style. Possible values are <ALIGN_LEFT>,
* <ALIGN_CENTER> and <ALIGN_RIGHT>. The value defines how any image in the
* vertex label is aligned horizontally within the label bounds of a
* <SHAPE_LABEL> shape. Value is "imageAlign".
*/
STYLE_IMAGE_ALIGN: 'imageAlign',
/**
* Variable: STYLE_IMAGE_VERTICAL_ALIGN
*
* Defines the key for the verticalAlign style. Possible values are
* <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM>. The value defines how
* any image in the vertex label is aligned vertically within the label
* bounds of a <SHAPE_LABEL> shape. Value is "imageVerticalAlign".
*/
STYLE_IMAGE_VERTICAL_ALIGN: 'imageVerticalAlign',
/**
* Variable: STYLE_GLASS
*
* Defines the key for the glass style. Possible values are 0 (disabled) and
* 1(enabled). The default value is 0. This is used in <mxLabel>. Value is
* "glass".
*/
STYLE_GLASS: 'glass',
/**
* Variable: STYLE_IMAGE
*
* Defines the key for the image style. Possible values are any image URL,
* the type of the value is String. This is the path to the image that is
* to be displayed within the label of a vertex. Data URLs should use the
* following format: data:image/png,xyz where xyz is the base64 encoded
* data (without the "base64"-prefix). Note that Data URLs are only
* supported in modern browsers. Value is "image".
*/
STYLE_IMAGE: 'image',
/**
* Variable: STYLE_IMAGE_WIDTH
*
* Defines the key for the imageWidth style. The type of this value is
* int, the value is the image width in pixels and must be greater than 0.
* Value is "imageWidth".
*/
STYLE_IMAGE_WIDTH: 'imageWidth',
/**
* Variable: STYLE_IMAGE_HEIGHT
*
* Defines the key for the imageHeight style. The type of this value is
* int, the value is the image height in pixels and must be greater than 0.
* Value is "imageHeight".
*/
STYLE_IMAGE_HEIGHT: 'imageHeight',
/**
* Variable: STYLE_IMAGE_BACKGROUND
*
* Defines the key for the image background color. This style is only used
* in <mxImageShape>. Possible values are all HTML color names or HEX
* codes. Value is "imageBackground".
*/
STYLE_IMAGE_BACKGROUND: 'imageBackground',
/**
* Variable: STYLE_IMAGE_BORDER
*
* Defines the key for the image border color. This style is only used in
* <mxImageShape>. Possible values are all HTML color names or HEX codes.
* Value is "imageBorder".
*/
STYLE_IMAGE_BORDER: 'imageBorder',
/**
* Variable: STYLE_FLIPH
*
* Defines the key for the horizontal image flip. This style is only used
* in <mxImageShape>. Possible values are 0 and 1. Default is 0. Value is
* "flipH".
*/
STYLE_FLIPH: 'flipH',
/**
* Variable: STYLE_FLIPV
*
* Defines the key for the vertical flip. Possible values are 0 and 1.
* Default is 0. Value is "flipV".
*/
STYLE_FLIPV: 'flipV',
/**
* Variable: STYLE_NOLABEL
*
* Defines the key for the noLabel style. If this is true then no label is
* visible for a given cell. Possible values are true or false (1 or 0).
* Default is false. Value is "noLabel".
*/
STYLE_NOLABEL: 'noLabel',
/**
* Variable: STYLE_NOEDGESTYLE
*
* Defines the key for the noEdgeStyle style. If this is true then no edge
* style is applied for a given edge. Possible values are true or false
* (1 or 0). Default is false. Value is "noEdgeStyle".
*/
STYLE_NOEDGESTYLE: 'noEdgeStyle',
/**
* Variable: STYLE_LABEL_BACKGROUNDCOLOR
*
* Defines the key for the label background color. Possible values are all
* HTML color names or HEX codes. Value is "labelBackgroundColor".
*/
STYLE_LABEL_BACKGROUNDCOLOR: 'labelBackgroundColor',
/**
* Variable: STYLE_LABEL_BORDERCOLOR
*
* Defines the key for the label border color. Possible values are all
* HTML color names or HEX codes. Value is "labelBorderColor".
*/
STYLE_LABEL_BORDERCOLOR: 'labelBorderColor',
/**
* Variable: STYLE_LABEL_PADDING
*
* Defines the key for the label padding, ie. the space between the label
* border and the label. Value is "labelPadding".
*/
STYLE_LABEL_PADDING: 'labelPadding',
/**
* Variable: STYLE_INDICATOR_SHAPE
*
* Defines the key for the indicator shape used within an <mxLabel>.
* Possible values are all SHAPE_* constants or the names of any new
* shapes. The indicatorShape has precedence over the indicatorImage.
* Value is "indicatorShape".
*/
STYLE_INDICATOR_SHAPE: 'indicatorShape',
/**
* Variable: STYLE_INDICATOR_IMAGE
*
* Defines the key for the indicator image used within an <mxLabel>.
* Possible values are all image URLs. The indicatorShape has
* precedence over the indicatorImage. Value is "indicatorImage".
*/
STYLE_INDICATOR_IMAGE: 'indicatorImage',
/**
* Variable: STYLE_INDICATOR_COLOR
*
* Defines the key for the indicatorColor style. Possible values are all
* HTML color names or HEX codes, as well as the special 'swimlane' keyword
* to refer to the color of the parent swimlane if one exists. Value is
* "indicatorColor".
*/
STYLE_INDICATOR_COLOR: 'indicatorColor',
/**
* Variable: STYLE_INDICATOR_STROKECOLOR
*
* Defines the key for the indicator stroke color in <mxLabel>.
* Possible values are all color codes. Value is "indicatorStrokeColor".
*/
STYLE_INDICATOR_STROKECOLOR: 'indicatorStrokeColor',
/**
* Variable: STYLE_INDICATOR_GRADIENTCOLOR
*
* Defines the key for the indicatorGradientColor style. Possible values
* are all HTML color names or HEX codes. This style is only supported in
* <SHAPE_LABEL> shapes. Value is "indicatorGradientColor".
*/
STYLE_INDICATOR_GRADIENTCOLOR: 'indicatorGradientColor',
/**
* Variable: STYLE_INDICATOR_SPACING
*
* The defines the key for the spacing between the label and the
* indicator in <mxLabel>. Possible values are in pixels. Value is
* "indicatorSpacing".
*/
STYLE_INDICATOR_SPACING: 'indicatorSpacing',
/**
* Variable: STYLE_INDICATOR_WIDTH
*
* Defines the key for the indicator width. Possible values start at 0 (in
* pixels). Value is "indicatorWidth".
*/
STYLE_INDICATOR_WIDTH: 'indicatorWidth',
/**
* Variable: STYLE_INDICATOR_HEIGHT
*
* Defines the key for the indicator height. Possible values start at 0 (in
* pixels). Value is "indicatorHeight".
*/
STYLE_INDICATOR_HEIGHT: 'indicatorHeight',
/**
* Variable: STYLE_INDICATOR_DIRECTION
*
* Defines the key for the indicatorDirection style. The direction style is
* used to specify the direction of certain shapes (eg. <mxTriangle>).
* Possible values are <DIRECTION_EAST> (default), <DIRECTION_WEST>,
* <DIRECTION_NORTH> and <DIRECTION_SOUTH>. Value is "indicatorDirection".
*/
STYLE_INDICATOR_DIRECTION: 'indicatorDirection',
/**
* Variable: STYLE_SHADOW
*
* Defines the key for the shadow style. The type of the value is Boolean.
* Value is "shadow".
*/
STYLE_SHADOW: 'shadow',
/**
* Variable: STYLE_SEGMENT
*
* Defines the key for the segment style. The type of this value is float
* and the value represents the size of the horizontal segment of the
* entity relation style. Default is ENTITY_SEGMENT. Value is "segment".
*/
STYLE_SEGMENT: 'segment',
/**
* Variable: STYLE_ENDARROW
*
* Defines the key for the end arrow marker. Possible values are all
* constants with an ARROW-prefix. This is only used in <mxConnector>.
* Value is "endArrow".
*
* Example:
* (code)
* style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC;
* (end)
*/
STYLE_ENDARROW: 'endArrow',
/**
* Variable: STYLE_STARTARROW
*
* Defines the key for the start arrow marker. Possible values are all
* constants with an ARROW-prefix. This is only used in <mxConnector>.
* See <STYLE_ENDARROW>. Value is "startArrow".
*/
STYLE_STARTARROW: 'startArrow',
/**
* Variable: STYLE_ENDSIZE
*
* Defines the key for the endSize style. The type of this value is numeric
* and the value represents the size of the end marker in pixels. Value is
* "endSize".
*/
STYLE_ENDSIZE: 'endSize',
/**
* Variable: STYLE_STARTSIZE
*
* Defines the key for the startSize style. The type of this value is
* numeric and the value represents the size of the start marker or the
* size of the swimlane title region depending on the shape it is used for.
* Value is "startSize".
*/
STYLE_STARTSIZE: 'startSize',
/**
* Variable: STYLE_SWIMLANE_LINE
*
* Defines the key for the swimlaneLine style. This style specifies whether
* the line between the title regio of a swimlane should be visible. Use 0
* for hidden or 1 (default) for visible. Value is "swimlaneLine".
*/
STYLE_SWIMLANE_LINE: 'swimlaneLine',
/**
* Variable: STYLE_ENDFILL
*
* Defines the key for the endFill style. Use 0 for no fill or 1 (default)
* for fill. (This style is only exported via <mxImageExport>.) Value is
* "endFill".
*/
STYLE_ENDFILL: 'endFill',
/**
* Variable: STYLE_STARTFILL
*
* Defines the key for the startFill style. Use 0 for no fill or 1 (default)
* for fill. (This style is only exported via <mxImageExport>.) Value is
* "startFill".
*/
STYLE_STARTFILL: 'startFill',
/**
* Variable: STYLE_DASHED
*
* Defines the key for the dashed style. Use 0 (default) for non-dashed or 1
* for dashed. Value is "dashed".
*/
STYLE_DASHED: 'dashed',
/**
* Defines the key for the dashed pattern style in SVG and image exports.
* The type of this value is a space separated list of numbers that specify
* a custom-defined dash pattern. Dash styles are defined in terms of the
* length of the dash (the drawn part of the stroke) and the length of the
* space between the dashes. The lengths are relative to the line width: a
* length of "1" is equal to the line width. VML ignores this style and
* uses dashStyle instead as defined in the VML specification. This style
* is only used in the <mxConnector> shape. Value is "dashPattern".
*/
STYLE_DASH_PATTERN: 'dashPattern',
/**
* Variable: STYLE_FIX_DASH
*
* Defines the key for the fixDash style. Use 0 (default) for dash patterns
* that depend on the linewidth and 1 for dash patterns that ignore the
* line width. Value is "fixDash".
*/
STYLE_FIX_DASH: 'fixDash',
/**
* Variable: STYLE_ROUNDED
*
* Defines the key for the rounded style. The type of this value is
* Boolean. For edges this determines whether or not joins between edges
* segments are smoothed to a rounded finish. For vertices that have the
* rectangle shape, this determines whether or not the rectangle is
* rounded. Use 0 (default) for non-rounded or 1 for rounded. Value is
* "rounded".
*/
STYLE_ROUNDED: 'rounded',
/**
* Variable: STYLE_CURVED
*
* Defines the key for the curved style. The type of this value is
* Boolean. It is only applicable for connector shapes. Use 0 (default)
* for non-curved or 1 for curved. Value is "curved".
*/
STYLE_CURVED: 'curved',
/**
* Variable: STYLE_ARCSIZE
*
* Defines the rounding factor for a rounded rectangle in percent (without
* the percent sign). Possible values are between 0 and 100. If this value
* is not specified then RECTANGLE_ROUNDING_FACTOR * 100 is used. For
* edges, this defines the absolute size of rounded corners in pixels. If
* this values is not specified then LINE_ARCSIZE is used.
* (This style is only exported via <mxImageExport>.) Value is "arcSize".
*/
STYLE_ARCSIZE: 'arcSize',
/**
* Variable: STYLE_ABSOLUTE_ARCSIZE
*
* Defines the key for the absolute arc size style. This specifies if
* arcSize for rectangles is abolute or relative. Possible values are 1
* and 0 (default). Value is "absoluteArcSize".
*/
STYLE_ABSOLUTE_ARCSIZE: 'absoluteArcSize',
/**
* Variable: STYLE_SOURCE_PERIMETER_SPACING
*
* Defines the key for the source perimeter spacing. The type of this value
* is numeric. This is the distance between the source connection point of
* an edge and the perimeter of the source vertex in pixels. This style
* only applies to edges. Value is "sourcePerimeterSpacing".
*/
STYLE_SOURCE_PERIMETER_SPACING: 'sourcePerimeterSpacing',
/**
* Variable: STYLE_TARGET_PERIMETER_SPACING
*
* Defines the key for the target perimeter spacing. The type of this value
* is numeric. This is the distance between the target connection point of
* an edge and the perimeter of the target vertex in pixels. This style
* only applies to edges. Value is "targetPerimeterSpacing".
*/
STYLE_TARGET_PERIMETER_SPACING: 'targetPerimeterSpacing',
/**
* Variable: STYLE_PERIMETER_SPACING
*
* Defines the key for the perimeter spacing. This is the distance between
* the connection point and the perimeter in pixels. When used in a vertex
* style, this applies to all incoming edges to floating ports (edges that
* terminate on the perimeter of the vertex). When used in an edge style,
* this spacing applies to the source and target separately, if they
* terminate in floating ports (on the perimeter of the vertex). Value is
* "perimeterSpacing".
*/
STYLE_PERIMETER_SPACING: 'perimeterSpacing',
/**
* Variable: STYLE_SPACING
*
* Defines the key for the spacing. The value represents the spacing, in
* pixels, added to each side of a label in a vertex (style applies to
* vertices only). Value is "spacing".
*/
STYLE_SPACING: 'spacing',
/**
* Variable: STYLE_SPACING_TOP
*
* Defines the key for the spacingTop style. The value represents the
* spacing, in pixels, added to the top side of a label in a vertex (style
* applies to vertices only). Value is "spacingTop".
*/
STYLE_SPACING_TOP: 'spacingTop',
/**
* Variable: STYLE_SPACING_LEFT
*
* Defines the key for the spacingLeft style. The value represents the
* spacing, in pixels, added to the left side of a label in a vertex (style
* applies to vertices only). Value is "spacingLeft".
*/
STYLE_SPACING_LEFT: 'spacingLeft',
/**
* Variable: STYLE_SPACING_BOTTOM
*
* Defines the key for the spacingBottom style The value represents the
* spacing, in pixels, added to the bottom side of a label in a vertex
* (style applies to vertices only). Value is "spacingBottom".
*/
STYLE_SPACING_BOTTOM: 'spacingBottom',
/**
* Variable: STYLE_SPACING_RIGHT
*
* Defines the key for the spacingRight style The value represents the
* spacing, in pixels, added to the right side of a label in a vertex (style
* applies to vertices only). Value is "spacingRight".
*/
STYLE_SPACING_RIGHT: 'spacingRight',
/**
* Variable: STYLE_HORIZONTAL
*
* Defines the key for the horizontal style. Possible values are
* true or false. This value only applies to vertices. If the <STYLE_SHAPE>
* is "SHAPE_SWIMLANE" a value of false indicates that the
* swimlane should be drawn vertically, true indicates to draw it
* horizontally. If the shape style does not indicate that this vertex is a
* swimlane, this value affects only whether the label is drawn
* horizontally or vertically. Value is "horizontal".
*/
STYLE_HORIZONTAL: 'horizontal',
/**
* Variable: STYLE_DIRECTION
*
* Defines the key for the direction style. The direction style is used
* to specify the direction of certain shapes (eg. <mxTriangle>).
* Possible values are <DIRECTION_EAST> (default), <DIRECTION_WEST>,
* <DIRECTION_NORTH> and <DIRECTION_SOUTH>. Value is "direction".
*/
STYLE_DIRECTION: 'direction',
/**
* Variable: STYLE_ANCHOR_POINT_DIRECTION
*
* Defines the key for the anchorPointDirection style. The defines if the
* direction style should be taken into account when computing the fixed
* point location for connected edges. Default is 1 (yes). Set this to 0
* to ignore the direction style for fixed connection points. Value is
* "anchorPointDirection".
*/
STYLE_ANCHOR_POINT_DIRECTION: 'anchorPointDirection',
/**
* Variable: STYLE_ELBOW
*
* Defines the key for the elbow style. Possible values are
* <ELBOW_HORIZONTAL> and <ELBOW_VERTICAL>. Default is <ELBOW_HORIZONTAL>.
* This defines how the three segment orthogonal edge style leaves its
* terminal vertices. The vertical style leaves the terminal vertices at
* the top and bottom sides. Value is "elbow".
*/
STYLE_ELBOW: 'elbow',
/**
* Variable: STYLE_FONTCOLOR
*
* Defines the key for the fontColor style. Possible values are all HTML
* color names or HEX codes. Value is "fontColor".
*/
STYLE_FONTCOLOR: 'fontColor',
/**
* Variable: STYLE_FONTFAMILY
*
* Defines the key for the fontFamily style. Possible values are names such
* as Arial; Dialog; Verdana; Times New Roman. The value is of type String.
* Value is fontFamily.
*/
STYLE_FONTFAMILY: 'fontFamily',
/**
* Variable: STYLE_FONTSIZE
*
* Defines the key for the fontSize style (in px). The type of the value
* is int. Value is "fontSize".
*/
STYLE_FONTSIZE: 'fontSize',
/**
* Variable: STYLE_FONTSTYLE
*
* Defines the key for the fontStyle style. Values may be any logical AND
* (sum) of <FONT_BOLD>, <FONT_ITALIC> and <FONT_UNDERLINE>.
* The type of the value is int. Value is "fontStyle".
*/
STYLE_FONTSTYLE: 'fontStyle',
/**
* Variable: STYLE_ASPECT
*
* Defines the key for the aspect style. Possible values are empty or fixed.
* If fixed is used then the aspect ratio of the cell will be maintained
* when resizing. Default is empty. Value is "aspect".
*/
STYLE_ASPECT: 'aspect',
/**
* Variable: STYLE_AUTOSIZE
*
* Defines the key for the autosize style. This specifies if a cell should be
* resized automatically if the value has changed. Possible values are 0 or 1.
* Default is 0. See <mxGraph.isAutoSizeCell>. This is normally combined with
* <STYLE_RESIZABLE> to disable manual sizing. Value is "autosize".
*/
STYLE_AUTOSIZE: 'autosize',
/**
* Variable: STYLE_FOLDABLE
*
* Defines the key for the foldable style. This specifies if a cell is foldable
* using a folding icon. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellFoldable>. Value is "foldable".
*/
STYLE_FOLDABLE: 'foldable',
/**
* Variable: STYLE_EDITABLE
*
* Defines the key for the editable style. This specifies if the value of
* a cell can be edited using the in-place editor. Possible values are 0 or
* 1. Default is 1. See <mxGraph.isCellEditable>. Value is "editable".
*/
STYLE_EDITABLE: 'editable',
/**
* Variable: STYLE_BACKGROUND_OUTLINE
*
* Defines the key for the backgroundOutline style. This specifies if a
* only the background of a cell should be painted when it is highlighted.
* Possible values are 0 or 1. Default is 0. Value is "backgroundOutline".
*/
STYLE_BACKGROUND_OUTLINE: 'backgroundOutline',
/**
* Variable: STYLE_BENDABLE
*
* Defines the key for the bendable style. This specifies if the control
* points of an edge can be moved. Possible values are 0 or 1. Default is
* 1. See <mxGraph.isCellBendable>. Value is "bendable".
*/
STYLE_BENDABLE: 'bendable',
/**
* Variable: STYLE_MOVABLE
*
* Defines the key for the movable style. This specifies if a cell can
* be moved. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellMovable>. Value is "movable".
*/
STYLE_MOVABLE: 'movable',
/**
* Variable: STYLE_RESIZABLE
*
* Defines the key for the resizable style. This specifies if a cell can
* be resized. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellResizable>. Value is "resizable".
*/
STYLE_RESIZABLE: 'resizable',
/**
* Variable: STYLE_RESIZE_WIDTH
*
* Defines the key for the resizeWidth style. This specifies if a cell's
* width is resized if the parent is resized. If this is 1 then the width
* will be resized even if the cell's geometry is relative. If this is 0
* then the cell's width will not be resized. Default is not defined. Value
* is "resizeWidth".
*/
STYLE_RESIZE_WIDTH: 'resizeWidth',
/**
* Variable: STYLE_RESIZE_WIDTH
*
* Defines the key for the resizeHeight style. This specifies if a cell's
* height if resize if the parent is resized. If this is 1 then the height
* will be resized even if the cell's geometry is relative. If this is 0
* then the cell's height will not be resized. Default is not defined. Value
* is "resizeHeight".
*/
STYLE_RESIZE_HEIGHT: 'resizeHeight',
/**
* Variable: STYLE_ROTATABLE
*
* Defines the key for the rotatable style. This specifies if a cell can
* be rotated. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellRotatable>. Value is "rotatable".
*/
STYLE_ROTATABLE: 'rotatable',
/**
* Variable: STYLE_CLONEABLE
*
* Defines the key for the cloneable style. This specifies if a cell can
* be cloned. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellCloneable>. Value is "cloneable".
*/
STYLE_CLONEABLE: 'cloneable',
/**
* Variable: STYLE_DELETABLE
*
* Defines the key for the deletable style. This specifies if a cell can be
* deleted. Possible values are 0 or 1. Default is 1. See
* <mxGraph.isCellDeletable>. Value is "deletable".
*/
STYLE_DELETABLE: 'deletable',
/**
* Variable: STYLE_SHAPE
*
* Defines the key for the shape. Possible values are all constants with
* a SHAPE-prefix or any newly defined shape names. Value is "shape".
*/
STYLE_SHAPE: 'shape',
/**
* Variable: STYLE_EDGE
*
* Defines the key for the edge style. Possible values are the functions
* defined in <mxEdgeStyle>. Value is "edgeStyle".
*/
STYLE_EDGE: 'edgeStyle',
/**
* Variable: STYLE_JETTY_SIZE
*
* Defines the key for the jetty size in <mxEdgeStyle.OrthConnector>.
* Default is 10. Possible values are all numeric values or "auto".
* Jetty size is the minimum length of the orthogonal segment before
* it attaches to a shape.
* Value is "jettySize".
*/
STYLE_JETTY_SIZE: 'jettySize',
/**
* Variable: STYLE_SOURCE_JETTY_SIZE
*
* Defines the key for the jetty size in <mxEdgeStyle.OrthConnector>.
* Default is 10. Possible values are numeric values or "auto". This has
* precedence over <STYLE_JETTY_SIZE>. Value is "sourceJettySize".
*/
STYLE_SOURCE_JETTY_SIZE: 'sourceJettySize',
/**
* Variable: targetJettySize
*
* Defines the key for the jetty size in <mxEdgeStyle.OrthConnector>.
* Default is 10. Possible values are numeric values or "auto". This has
* precedence over <STYLE_JETTY_SIZE>. Value is "targetJettySize".
*/
STYLE_TARGET_JETTY_SIZE: 'targetJettySize',
/**
* Variable: STYLE_LOOP
*
* Defines the key for the loop style. Possible values are the functions
* defined in <mxEdgeStyle>. Value is "loopStyle". Default is
* <mxGraph.defaultLoopStylean>.
*/
STYLE_LOOP: 'loopStyle',
/**
* Variable: STYLE_ORTHOGONAL_LOOP
*
* Defines the key for the orthogonal loop style. Possible values are 0 and
* 1. Default is 0. Value is "orthogonalLoop". Use this style to specify
* if loops with no waypoints and defined anchor points should be routed
* using <STYLE_LOOP> or not routed.
*/
STYLE_ORTHOGONAL_LOOP: 'orthogonalLoop',
/**
* Variable: STYLE_ROUTING_CENTER_X
*
* Defines the key for the horizontal routing center. Possible values are
* between -0.5 and 0.5. This is the relative offset from the center used
* for connecting edges. The type of this value is numeric. Value is
* "routingCenterX".
*/
STYLE_ROUTING_CENTER_X: 'routingCenterX',
/**
* Variable: STYLE_ROUTING_CENTER_Y
*
* Defines the key for the vertical routing center. Possible values are
* between -0.5 and 0.5. This is the relative offset from the center used
* for connecting edges. The type of this value is numeric. Value is
* "routingCenterY".
*/
STYLE_ROUTING_CENTER_Y: 'routingCenterY',
/**
* Variable: FONT_BOLD
*
* Constant for bold fonts. Default is 1.
*/
FONT_BOLD: 1,
/**
* Variable: FONT_ITALIC
*
* Constant for italic fonts. Default is 2.
*/
FONT_ITALIC: 2,
/**
* Variable: FONT_UNDERLINE
*
* Constant for underlined fonts. Default is 4.
*/
FONT_UNDERLINE: 4,
/**
* Variable: SHAPE_RECTANGLE
*
* Name under which <mxRectangleShape> is registered in <mxCellRenderer>.
* Default is rectangle.
*/
SHAPE_RECTANGLE: 'rectangle',
/**
* Variable: SHAPE_ELLIPSE
*
* Name under which <mxEllipse> is registered in <mxCellRenderer>.
* Default is ellipse.
*/
SHAPE_ELLIPSE: 'ellipse',
/**
* Variable: SHAPE_DOUBLE_ELLIPSE
*
* Name under which <mxDoubleEllipse> is registered in <mxCellRenderer>.
* Default is doubleEllipse.
*/
SHAPE_DOUBLE_ELLIPSE: 'doubleEllipse',
/**
* Variable: SHAPE_RHOMBUS
*
* Name under which <mxRhombus> is registered in <mxCellRenderer>.
* Default is rhombus.
*/
SHAPE_RHOMBUS: 'rhombus',
/**
* Variable: SHAPE_LINE
*
* Name under which <mxLine> is registered in <mxCellRenderer>.
* Default is line.
*/
SHAPE_LINE: 'line',
/**
* Variable: SHAPE_IMAGE
*
* Name under which <mxImageShape> is registered in <mxCellRenderer>.
* Default is image.
*/
SHAPE_IMAGE: 'image',
/**
* Variable: SHAPE_ARROW
*
* Name under which <mxArrow> is registered in <mxCellRenderer>.
* Default is arrow.
*/
SHAPE_ARROW: 'arrow',
/**
* Variable: SHAPE_ARROW_CONNECTOR
*
* Name under which <mxArrowConnector> is registered in <mxCellRenderer>.
* Default is arrowConnector.
*/
SHAPE_ARROW_CONNECTOR: 'arrowConnector',
/**
* Variable: SHAPE_LABEL
*
* Name under which <mxLabel> is registered in <mxCellRenderer>.
* Default is label.
*/
SHAPE_LABEL: 'label',
/**
* Variable: SHAPE_CYLINDER
*
* Name under which <mxCylinder> is registered in <mxCellRenderer>.
* Default is cylinder.
*/
SHAPE_CYLINDER: 'cylinder',
/**
* Variable: SHAPE_SWIMLANE
*
* Name under which <mxSwimlane> is registered in <mxCellRenderer>.
* Default is swimlane.
*/
SHAPE_SWIMLANE: 'swimlane',
/**
* Variable: SHAPE_CONNECTOR
*
* Name under which <mxConnector> is registered in <mxCellRenderer>.
* Default is connector.
*/
SHAPE_CONNECTOR: 'connector',
/**
* Variable: SHAPE_ACTOR
*
* Name under which <mxActor> is registered in <mxCellRenderer>.
* Default is actor.
*/
SHAPE_ACTOR: 'actor',
/**
* Variable: SHAPE_CLOUD
*
* Name under which <mxCloud> is registered in <mxCellRenderer>.
* Default is cloud.
*/
SHAPE_CLOUD: 'cloud',
/**
* Variable: SHAPE_TRIANGLE
*
* Name under which <mxTriangle> is registered in <mxCellRenderer>.
* Default is triangle.
*/
SHAPE_TRIANGLE: 'triangle',
/**
* Variable: SHAPE_HEXAGON
*
* Name under which <mxHexagon> is registered in <mxCellRenderer>.
* Default is hexagon.
*/
SHAPE_HEXAGON: 'hexagon',
/**
* Variable: ARROW_CLASSIC
*
* Constant for classic arrow markers.
*/
ARROW_CLASSIC: 'classic',
/**
* Variable: ARROW_CLASSIC_THIN
*
* Constant for thin classic arrow markers.
*/
ARROW_CLASSIC_THIN: 'classicThin',
/**
* Variable: ARROW_BLOCK
*
* Constant for block arrow markers.
*/
ARROW_BLOCK: 'block',
/**
* Variable: ARROW_BLOCK_THIN
*
* Constant for thin block arrow markers.
*/
ARROW_BLOCK_THIN: 'blockThin',
/**
* Variable: ARROW_OPEN
*
* Constant for open arrow markers.
*/
ARROW_OPEN: 'open',
/**
* Variable: ARROW_OPEN_THIN
*
* Constant for thin open arrow markers.
*/
ARROW_OPEN_THIN: 'openThin',
/**
* Variable: ARROW_OVAL
*
* Constant for oval arrow markers.
*/
ARROW_OVAL: 'oval',
/**
* Variable: ARROW_DIAMOND
*
* Constant for diamond arrow markers.
*/
ARROW_DIAMOND: 'diamond',
/**
* Variable: ARROW_DIAMOND_THIN
*
* Constant for thin diamond arrow markers.
*/
ARROW_DIAMOND_THIN: 'diamondThin',
/**
* Variable: ALIGN_LEFT
*
* Constant for left horizontal alignment. Default is left.
*/
ALIGN_LEFT: 'left',
/**
* Variable: ALIGN_CENTER
*
* Constant for center horizontal alignment. Default is center.
*/
ALIGN_CENTER: 'center',
/**
* Variable: ALIGN_RIGHT
*
* Constant for right horizontal alignment. Default is right.
*/
ALIGN_RIGHT: 'right',
/**
* Variable: ALIGN_TOP
*
* Constant for top vertical alignment. Default is top.
*/
ALIGN_TOP: 'top',
/**
* Variable: ALIGN_MIDDLE
*
* Constant for middle vertical alignment. Default is middle.
*/
ALIGN_MIDDLE: 'middle',
/**
* Variable: ALIGN_BOTTOM
*
* Constant for bottom vertical alignment. Default is bottom.
*/
ALIGN_BOTTOM: 'bottom',
/**
* Variable: DIRECTION_NORTH
*
* Constant for direction north. Default is north.
*/
DIRECTION_NORTH: 'north',
/**
* Variable: DIRECTION_SOUTH
*
* Constant for direction south. Default is south.
*/
DIRECTION_SOUTH: 'south',
/**
* Variable: DIRECTION_EAST
*
* Constant for direction east. Default is east.
*/
DIRECTION_EAST: 'east',
/**
* Variable: DIRECTION_WEST
*
* Constant for direction west. Default is west.
*/
DIRECTION_WEST: 'west',
/**
* Variable: TEXT_DIRECTION_DEFAULT
*
* Constant for text direction default. Default is an empty string. Use
* this value to use the default text direction of the operating system.
*/
TEXT_DIRECTION_DEFAULT: '',
/**
* Variable: TEXT_DIRECTION_AUTO
*
* Constant for text direction automatic. Default is auto. Use this value
* to find the direction for a given text with <mxText.getAutoDirection>.
*/
TEXT_DIRECTION_AUTO: 'auto',
/**
* Variable: TEXT_DIRECTION_LTR
*
* Constant for text direction left to right. Default is ltr. Use this
* value for left to right text direction.
*/
TEXT_DIRECTION_LTR: 'ltr',
/**
* Variable: TEXT_DIRECTION_RTL
*
* Constant for text direction right to left. Default is rtl. Use this
* value for right to left text direction.
*/
TEXT_DIRECTION_RTL: 'rtl',
/**
* Variable: DIRECTION_MASK_NONE
*
* Constant for no direction.
*/
DIRECTION_MASK_NONE: 0,
/**
* Variable: DIRECTION_MASK_WEST
*
* Bitwise mask for west direction.
*/
DIRECTION_MASK_WEST: 1,
/**
* Variable: DIRECTION_MASK_NORTH
*
* Bitwise mask for north direction.
*/
DIRECTION_MASK_NORTH: 2,
/**
* Variable: DIRECTION_MASK_SOUTH
*
* Bitwise mask for south direction.
*/
DIRECTION_MASK_SOUTH: 4,
/**
* Variable: DIRECTION_MASK_EAST
*
* Bitwise mask for east direction.
*/
DIRECTION_MASK_EAST: 8,
/**
* Variable: DIRECTION_MASK_ALL
*
* Bitwise mask for all directions.
*/
DIRECTION_MASK_ALL: 15,
/**
* Variable: ELBOW_VERTICAL
*
* Constant for elbow vertical. Default is horizontal.
*/
ELBOW_VERTICAL: 'vertical',
/**
* Variable: ELBOW_HORIZONTAL
*
* Constant for elbow horizontal. Default is horizontal.
*/
ELBOW_HORIZONTAL: 'horizontal',
/**
* Variable: EDGESTYLE_ELBOW
*
* Name of the elbow edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_ELBOW: 'elbowEdgeStyle',
/**
* Variable: EDGESTYLE_ENTITY_RELATION
*
* Name of the entity relation edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_ENTITY_RELATION: 'entityRelationEdgeStyle',
/**
* Variable: EDGESTYLE_LOOP
*
* Name of the loop edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_LOOP: 'loopEdgeStyle',
/**
* Variable: EDGESTYLE_SIDETOSIDE
*
* Name of the side to side edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_SIDETOSIDE: 'sideToSideEdgeStyle',
/**
* Variable: EDGESTYLE_TOPTOBOTTOM
*
* Name of the top to bottom edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_TOPTOBOTTOM: 'topToBottomEdgeStyle',
/**
* Variable: EDGESTYLE_ORTHOGONAL
*
* Name of the generic orthogonal edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_ORTHOGONAL: 'orthogonalEdgeStyle',
/**
* Variable: EDGESTYLE_SEGMENT
*
* Name of the generic segment edge style. Can be used as a string value
* for the STYLE_EDGE style.
*/
EDGESTYLE_SEGMENT: 'segmentEdgeStyle',
/**
* Variable: PERIMETER_ELLIPSE
*
* Name of the ellipse perimeter. Can be used as a string value
* for the STYLE_PERIMETER style.
*/
PERIMETER_ELLIPSE: 'ellipsePerimeter',
/**
* Variable: PERIMETER_RECTANGLE
*
* Name of the rectangle perimeter. Can be used as a string value
* for the STYLE_PERIMETER style.
*/
PERIMETER_RECTANGLE: 'rectanglePerimeter',
/**
* Variable: PERIMETER_RHOMBUS
*
* Name of the rhombus perimeter. Can be used as a string value
* for the STYLE_PERIMETER style.
*/
PERIMETER_RHOMBUS: 'rhombusPerimeter',
/**
* Variable: PERIMETER_HEXAGON
*
* Name of the hexagon perimeter. Can be used as a string value
* for the STYLE_PERIMETER style.
*/
PERIMETER_HEXAGON: 'hexagonPerimeter',
/**
* Variable: PERIMETER_TRIANGLE
*
* Name of the triangle perimeter. Can be used as a string value
* for the STYLE_PERIMETER style.
*/
PERIMETER_TRIANGLE: 'trianglePerimeter'
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEventObject
*
* The mxEventObject is a wrapper for all properties of a single event.
* Additionally, it also offers functions to consume the event and check if it
* was consumed as follows:
*
* (code)
* evt.consume();
* INV: evt.isConsumed() == true
* (end)
*
* Constructor: mxEventObject
*
* Constructs a new event object with the specified name. An optional
* sequence of key, value pairs can be appended to define properties.
*
* Example:
*
* (code)
* new mxEventObject("eventName", key1, val1, .., keyN, valN)
* (end)
*/
function mxEventObject(name)
{
this.name = name;
this.properties = [];
for (var i = 1; i < arguments.length; i += 2)
{
if (arguments[i + 1] != null)
{
this.properties[arguments[i]] = arguments[i + 1];
}
}
};
/**
* Variable: name
*
* Holds the name.
*/
mxEventObject.prototype.name = null;
/**
* Variable: properties
*
* Holds the properties as an associative array.
*/
mxEventObject.prototype.properties = null;
/**
* Variable: consumed
*
* Holds the consumed state. Default is false.
*/
mxEventObject.prototype.consumed = false;
/**
* Function: getName
*
* Returns <name>.
*/
mxEventObject.prototype.getName = function()
{
return this.name;
};
/**
* Function: getProperties
*
* Returns <properties>.
*/
mxEventObject.prototype.getProperties = function()
{
return this.properties;
};
/**
* Function: getProperty
*
* Returns the property for the given key.
*/
mxEventObject.prototype.getProperty = function(key)
{
return this.properties[key];
};
/**
* Function: isConsumed
*
* Returns true if the event has been consumed.
*/
mxEventObject.prototype.isConsumed = function()
{
return this.consumed;
};
/**
* Function: consume
*
* Consumes the event.
*/
mxEventObject.prototype.consume = function()
{
this.consumed = true;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMouseEvent
*
* Base class for all mouse events in mxGraph. A listener for this event should
* implement the following methods:
*
* (code)
* graph.addMouseListener(
* {
* mouseDown: function(sender, evt)
* {
* mxLog.debug('mouseDown');
* },
* mouseMove: function(sender, evt)
* {
* mxLog.debug('mouseMove');
* },
* mouseUp: function(sender, evt)
* {
* mxLog.debug('mouseUp');
* }
* });
* (end)
*
* Constructor: mxMouseEvent
*
* Constructs a new event object for the given arguments.
*
* Parameters:
*
* evt - Native mouse event.
* state - Optional <mxCellState> under the mouse.
*
*/
function mxMouseEvent(evt, state)
{
this.evt = evt;
this.state = state;
this.sourceState = state;
};
/**
* Variable: consumed
*
* Holds the consumed state of this event.
*/
mxMouseEvent.prototype.consumed = false;
/**
* Variable: evt
*
* Holds the inner event object.
*/
mxMouseEvent.prototype.evt = null;
/**
* Variable: graphX
*
* Holds the x-coordinate of the event in the graph. This value is set in
* <mxGraph.fireMouseEvent>.
*/
mxMouseEvent.prototype.graphX = null;
/**
* Variable: graphY
*
* Holds the y-coordinate of the event in the graph. This value is set in
* <mxGraph.fireMouseEvent>.
*/
mxMouseEvent.prototype.graphY = null;
/**
* Variable: state
*
* Holds the optional <mxCellState> associated with this event.
*/
mxMouseEvent.prototype.state = null;
/**
* Variable: sourceState
*
* Holds the <mxCellState> that was passed to the constructor. This can be
* different from <state> depending on the result of <mxGraph.getEventState>.
*/
mxMouseEvent.prototype.sourceState = null;
/**
* Function: getEvent
*
* Returns <evt>.
*/
mxMouseEvent.prototype.getEvent = function()
{
return this.evt;
};
/**
* Function: getSource
*
* Returns the target DOM element using <mxEvent.getSource> for <evt>.
*/
mxMouseEvent.prototype.getSource = function()
{
return mxEvent.getSource(this.evt);
};
/**
* Function: isSource
*
* Returns true if the given <mxShape> is the source of <evt>.
*/
mxMouseEvent.prototype.isSource = function(shape)
{
if (shape != null)
{
return mxUtils.isAncestorNode(shape.node, this.getSource());
}
return false;
};
/**
* Function: getX
*
* Returns <evt.clientX>.
*/
mxMouseEvent.prototype.getX = function()
{
return mxEvent.getClientX(this.getEvent());
};
/**
* Function: getY
*
* Returns <evt.clientY>.
*/
mxMouseEvent.prototype.getY = function()
{
return mxEvent.getClientY(this.getEvent());
};
/**
* Function: getGraphX
*
* Returns <graphX>.
*/
mxMouseEvent.prototype.getGraphX = function()
{
return this.graphX;
};
/**
* Function: getGraphY
*
* Returns <graphY>.
*/
mxMouseEvent.prototype.getGraphY = function()
{
return this.graphY;
};
/**
* Function: getState
*
* Returns <state>.
*/
mxMouseEvent.prototype.getState = function()
{
return this.state;
};
/**
* Function: getCell
*
* Returns the <mxCell> in <state> is not null.
*/
mxMouseEvent.prototype.getCell = function()
{
var state = this.getState();
if (state != null)
{
return state.cell;
}
return null;
};
/**
* Function: isPopupTrigger
*
* Returns true if the event is a popup trigger.
*/
mxMouseEvent.prototype.isPopupTrigger = function()
{
return mxEvent.isPopupTrigger(this.getEvent());
};
/**
* Function: isConsumed
*
* Returns <consumed>.
*/
mxMouseEvent.prototype.isConsumed = function()
{
return this.consumed;
};
/**
* Function: consume
*
* Sets <consumed> to true and invokes preventDefault on the native event
* if such a method is defined. This is used mainly to avoid the cursor from
* being changed to a text cursor in Webkit. You can use the preventDefault
* flag to disable this functionality.
*
* Parameters:
*
* preventDefault - Specifies if the native event should be canceled. Default
* is true.
*/
mxMouseEvent.prototype.consume = function(preventDefault)
{
preventDefault = (preventDefault != null) ? preventDefault :
(this.evt.touches != null || mxEvent.isMouseEvent(this.evt));
if (preventDefault && this.evt.preventDefault)
{
this.evt.preventDefault();
}
// Workaround for images being dragged in IE
// Does not change returnValue in Opera
if (mxClient.IS_IE)
{
this.evt.returnValue = true;
}
// Sets local consumed state
this.consumed = true;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEventSource
*
* Base class for objects that dispatch named events. To create a subclass that
* inherits from mxEventSource, the following code is used.
*
* (code)
* function MyClass() { };
*
* MyClass.prototype = new mxEventSource();
* MyClass.prototype.constructor = MyClass;
* (end)
*
* Known Subclasses:
*
* <mxGraphModel>, <mxGraph>, <mxGraphView>, <mxEditor>, <mxCellOverlay>,
* <mxToolbar>, <mxWindow>
*
* Constructor: mxEventSource
*
* Constructs a new event source.
*/
function mxEventSource(eventSource)
{
this.setEventSource(eventSource);
};
/**
* Variable: eventListeners
*
* Holds the event names and associated listeners in an array. The array
* contains the event name followed by the respective listener for each
* registered listener.
*/
mxEventSource.prototype.eventListeners = null;
/**
* Variable: eventsEnabled
*
* Specifies if events can be fired. Default is true.
*/
mxEventSource.prototype.eventsEnabled = true;
/**
* Variable: eventSource
*
* Optional source for events. Default is null.
*/
mxEventSource.prototype.eventSource = null;
/**
* Function: isEventsEnabled
*
* Returns <eventsEnabled>.
*/
mxEventSource.prototype.isEventsEnabled = function()
{
return this.eventsEnabled;
};
/**
* Function: setEventsEnabled
*
* Sets <eventsEnabled>.
*/
mxEventSource.prototype.setEventsEnabled = function(value)
{
this.eventsEnabled = value;
};
/**
* Function: getEventSource
*
* Returns <eventSource>.
*/
mxEventSource.prototype.getEventSource = function()
{
return this.eventSource;
};
/**
* Function: setEventSource
*
* Sets <eventSource>.
*/
mxEventSource.prototype.setEventSource = function(value)
{
this.eventSource = value;
};
/**
* Function: addListener
*
* Binds the specified function to the given event name. If no event name
* is given, then the listener is registered for all events.
*
* The parameters of the listener are the sender and an <mxEventObject>.
*/
mxEventSource.prototype.addListener = function(name, funct)
{
if (this.eventListeners == null)
{
this.eventListeners = [];
}
this.eventListeners.push(name);
this.eventListeners.push(funct);
};
/**
* Function: removeListener
*
* Removes all occurrences of the given listener from <eventListeners>.
*/
mxEventSource.prototype.removeListener = function(funct)
{
if (this.eventListeners != null)
{
var i = 0;
while (i < this.eventListeners.length)
{
if (this.eventListeners[i+1] == funct)
{
this.eventListeners.splice(i, 2);
}
else
{
i += 2;
}
}
}
};
/**
* Function: fireEvent
*
* Dispatches the given event to the listeners which are registered for
* the event. The sender argument is optional. The current execution scope
* ("this") is used for the listener invocation (see <mxUtils.bind>).
*
* Example:
*
* (code)
* fireEvent(new mxEventObject("eventName", key1, val1, .., keyN, valN))
* (end)
*
* Parameters:
*
* evt - <mxEventObject> that represents the event.
* sender - Optional sender to be passed to the listener. Default value is
* the return value of <getEventSource>.
*/
mxEventSource.prototype.fireEvent = function(evt, sender)
{
if (this.eventListeners != null && this.isEventsEnabled())
{
if (evt == null)
{
evt = new mxEventObject();
}
if (sender == null)
{
sender = this.getEventSource();
}
if (sender == null)
{
sender = this;
}
var args = [sender, evt];
for (var i = 0; i < this.eventListeners.length; i += 2)
{
var listen = this.eventListeners[i];
if (listen == null || listen == evt.getName())
{
this.eventListeners[i+1].apply(this, args);
}
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxEvent =
{
/**
* Class: mxEvent
*
* Cross-browser DOM event support. For internal event handling,
* <mxEventSource> and the graph event dispatch loop in <mxGraph> are used.
*
* Memory Leaks:
*
* Use this class for adding and removing listeners to/from DOM nodes. The
* <removeAllListeners> function is provided to remove all listeners that
* have been added using <addListener>. The function should be invoked when
* the last reference is removed in the JavaScript code, typically when the
* referenced DOM node is removed from the DOM.
*
* Function: addListener
*
* Binds the function to the specified event on the given element. Use
* <mxUtils.bind> in order to bind the "this" keyword inside the function
* to a given execution scope.
*/
addListener: function()
{
var updateListenerList = function(element, eventName, funct)
{
if (element.mxListenerList == null)
{
element.mxListenerList = [];
}
var entry = {name: eventName, f: funct};
element.mxListenerList.push(entry);
};
if (window.addEventListener)
{
return function(element, eventName, funct)
{
element.addEventListener(eventName, funct, false);
updateListenerList(element, eventName, funct);
};
}
else
{
return function(element, eventName, funct)
{
element.attachEvent('on' + eventName, funct);
updateListenerList(element, eventName, funct);
};
}
}(),
/**
* Function: removeListener
*
* Removes the specified listener from the given element.
*/
removeListener: function()
{
var updateListener = function(element, eventName, funct)
{
if (element.mxListenerList != null)
{
var listenerCount = element.mxListenerList.length;
for (var i = 0; i < listenerCount; i++)
{
var entry = element.mxListenerList[i];
if (entry.f == funct)
{
element.mxListenerList.splice(i, 1);
break;
}
}
if (element.mxListenerList.length == 0)
{
element.mxListenerList = null;
}
}
};
if (window.removeEventListener)
{
return function(element, eventName, funct)
{
element.removeEventListener(eventName, funct, false);
updateListener(element, eventName, funct);
};
}
else
{
return function(element, eventName, funct)
{
element.detachEvent('on' + eventName, funct);
updateListener(element, eventName, funct);
};
}
}(),
/**
* Function: removeAllListeners
*
* Removes all listeners from the given element.
*/
removeAllListeners: function(element)
{
var list = element.mxListenerList;
if (list != null)
{
while (list.length > 0)
{
var entry = list[0];
mxEvent.removeListener(element, entry.name, entry.f);
}
}
},
/**
* Function: addGestureListeners
*
* Adds the given listeners for touch, mouse and/or pointer events. If
* <mxClient.IS_POINTER> is true then pointer events will be registered,
* else the respective mouse events will be registered. If <mxClient.IS_POINTER>
* is false and <mxClient.IS_TOUCH> is true then the respective touch events
* will be registered as well as the mouse events.
*/
addGestureListeners: function(node, startListener, moveListener, endListener)
{
if (startListener != null)
{
mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', startListener);
}
if (moveListener != null)
{
mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', moveListener);
}
if (endListener != null)
{
mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', endListener);
}
if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
{
if (startListener != null)
{
mxEvent.addListener(node, 'touchstart', startListener);
}
if (moveListener != null)
{
mxEvent.addListener(node, 'touchmove', moveListener);
}
if (endListener != null)
{
mxEvent.addListener(node, 'touchend', endListener);
}
}
},
/**
* Function: removeGestureListeners
*
* Removes the given listeners from mousedown, mousemove, mouseup and the
* respective touch events if <mxClient.IS_TOUCH> is true.
*/
removeGestureListeners: function(node, startListener, moveListener, endListener)
{
if (startListener != null)
{
mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', startListener);
}
if (moveListener != null)
{
mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', moveListener);
}
if (endListener != null)
{
mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', endListener);
}
if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
{
if (startListener != null)
{
mxEvent.removeListener(node, 'touchstart', startListener);
}
if (moveListener != null)
{
mxEvent.removeListener(node, 'touchmove', moveListener);
}
if (endListener != null)
{
mxEvent.removeListener(node, 'touchend', endListener);
}
}
},
/**
* Function: redirectMouseEvents
*
* Redirects the mouse events from the given DOM node to the graph dispatch
* loop using the event and given state as event arguments. State can
* either be an instance of <mxCellState> or a function that returns an
* <mxCellState>. The down, move, up and dblClick arguments are optional
* functions that take the trigger event as arguments and replace the
* default behaviour.
*/
redirectMouseEvents: function(node, graph, state, down, move, up, dblClick)
{
var getState = function(evt)
{
return (typeof(state) == 'function') ? state(evt) : state;
};
mxEvent.addGestureListeners(node, function (evt)
{
if (down != null)
{
down(evt);
}
else if (!mxEvent.isConsumed(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, getState(evt)));
}
},
function (evt)
{
if (move != null)
{
move(evt);
}
else if (!mxEvent.isConsumed(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
}
},
function (evt)
{
if (up != null)
{
up(evt);
}
else if (!mxEvent.isConsumed(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
}
});
mxEvent.addListener(node, 'dblclick', function (evt)
{
if (dblClick != null)
{
dblClick(evt);
}
else if (!mxEvent.isConsumed(evt))
{
var tmp = getState(evt);
graph.dblClick(evt, (tmp != null) ? tmp.cell : null);
}
});
},
/**
* Function: release
*
* Removes the known listeners from the given DOM node and its descendants.
*
* Parameters:
*
* element - DOM node to remove the listeners from.
*/
release: function(element)
{
try
{
if (element != null)
{
mxEvent.removeAllListeners(element);
var children = element.childNodes;
if (children != null)
{
var childCount = children.length;
for (var i = 0; i < childCount; i += 1)
{
mxEvent.release(children[i]);
}
}
}
}
catch (e)
{
// ignores errors as this is typically called in cleanup code
}
},
/**
* Function: addMouseWheelListener
*
* Installs the given function as a handler for mouse wheel events. The
* function has two arguments: the mouse event and a boolean that specifies
* if the wheel was moved up or down.
*
* This has been tested with IE 6 and 7, Firefox (all versions), Opera and
* Safari. It does currently not work on Safari for Mac.
*
* Example:
*
* (code)
* mxEvent.addMouseWheelListener(function (evt, up)
* {
* mxLog.show();
* mxLog.debug('mouseWheel: up='+up);
* });
*(end)
*
* Parameters:
*
* funct - Handler function that takes the event argument and a boolean up
* argument for the mousewheel direction.
* target - Target for installing the listener in Google Chrome. See
* https://www.chromestatus.com/features/6662647093133312.
*/
addMouseWheelListener: function(funct, target)
{
if (funct != null)
{
var wheelHandler = function(evt)
{
// IE does not give an event object but the
// global event object is the mousewheel event
// at this point in time.
if (evt == null)
{
evt = window.event;
}
var delta = 0;
if (mxClient.IS_FF)
{
delta = -evt.detail / 2;
}
else
{
delta = evt.wheelDelta / 120;
}
// Handles the event using the given function
if (delta != 0)
{
funct(evt, delta > 0);
}
};
// Webkit has NS event API, but IE event name and details
if (mxClient.IS_NS && document.documentMode == null)
{
var eventName = (mxClient.IS_SF || mxClient.IS_GC) ? 'mousewheel' : 'DOMMouseScroll';
mxEvent.addListener((mxClient.IS_GC && target != null) ? target : window,
eventName, wheelHandler);
}
else
{
mxEvent.addListener(document, 'mousewheel', wheelHandler);
}
}
},
/**
* Function: disableContextMenu
*
* Disables the context menu for the given element.
*/
disableContextMenu: function(element)
{
mxEvent.addListener(element, 'contextmenu', function(evt)
{
if (evt.preventDefault)
{
evt.preventDefault();
}
return false;
});
},
/**
* Function: getSource
*
* Returns the event's target or srcElement depending on the browser.
*/
getSource: function(evt)
{
return (evt.srcElement != null) ? evt.srcElement : evt.target;
},
/**
* Function: isConsumed
*
* Returns true if the event has been consumed using <consume>.
*/
isConsumed: function(evt)
{
return evt.isConsumed != null && evt.isConsumed;
},
/**
* Function: isTouchEvent
*
* Returns true if the event was generated using a touch device (not a pen or mouse).
*/
isTouchEvent: function(evt)
{
return (evt.pointerType != null) ? (evt.pointerType == 'touch' || evt.pointerType ===
evt.MSPOINTER_TYPE_TOUCH) : ((evt.mozInputSource != null) ?
evt.mozInputSource == 5 : evt.type.indexOf('touch') == 0);
},
/**
* Function: isPenEvent
*
* Returns true if the event was generated using a pen (not a touch device or mouse).
*/
isPenEvent: function(evt)
{
return (evt.pointerType != null) ? (evt.pointerType == 'pen' || evt.pointerType ===
evt.MSPOINTER_TYPE_PEN) : ((evt.mozInputSource != null) ?
evt.mozInputSource == 2 : evt.type.indexOf('pen') == 0);
},
/**
* Function: isMultiTouchEvent
*
* Returns true if the event was generated using a touch device (not a pen or mouse).
*/
isMultiTouchEvent: function(evt)
{
return (evt.type != null && evt.type.indexOf('touch') == 0 && evt.touches != null && evt.touches.length > 1);
},
/**
* Function: isMouseEvent
*
* Returns true if the event was generated using a mouse (not a pen or touch device).
*/
isMouseEvent: function(evt)
{
return (evt.pointerType != null) ? (evt.pointerType == 'mouse' || evt.pointerType ===
evt.MSPOINTER_TYPE_MOUSE) : ((evt.mozInputSource != null) ?
evt.mozInputSource == 1 : evt.type.indexOf('mouse') == 0);
},
/**
* Function: isLeftMouseButton
*
* Returns true if the left mouse button is pressed for the given event.
* To check if a button is pressed during a mouseMove you should use the
* <mxGraph.isMouseDown> property. Note that this returns true in Firefox
* for control+left-click on the Mac.
*/
isLeftMouseButton: function(evt)
{
// Special case for mousemove and mousedown we check the buttons
// if it exists because which is 0 even if no button is pressed
if ('buttons' in evt && (evt.type == 'mousedown' || evt.type == 'mousemove'))
{
return evt.buttons == 1;
}
else if ('which' in evt)
{
return evt.which === 1;
}
else
{
return evt.button === 1;
}
},
/**
* Function: isMiddleMouseButton
*
* Returns true if the middle mouse button is pressed for the given event.
* To check if a button is pressed during a mouseMove you should use the
* <mxGraph.isMouseDown> property.
*/
isMiddleMouseButton: function(evt)
{
if ('which' in evt)
{
return evt.which === 2;
}
else
{
return evt.button === 4;
}
},
/**
* Function: isRightMouseButton
*
* Returns true if the right mouse button was pressed. Note that this
* button might not be available on some systems. For handling a popup
* trigger <isPopupTrigger> should be used.
*/
isRightMouseButton: function(evt)
{
if ('which' in evt)
{
return evt.which === 3;
}
else
{
return evt.button === 2;
}
},
/**
* Function: isPopupTrigger
*
* Returns true if the event is a popup trigger. This implementation
* returns true if the right button or the left button and control was
* pressed on a Mac.
*/
isPopupTrigger: function(evt)
{
return mxEvent.isRightMouseButton(evt) || (mxClient.IS_MAC && mxEvent.isControlDown(evt) &&
!mxEvent.isShiftDown(evt) && !mxEvent.isMetaDown(evt) && !mxEvent.isAltDown(evt));
},
/**
* Function: isShiftDown
*
* Returns true if the shift key is pressed for the given event.
*/
isShiftDown: function(evt)
{
return (evt != null) ? evt.shiftKey : false;
},
/**
* Function: isAltDown
*
* Returns true if the alt key is pressed for the given event.
*/
isAltDown: function(evt)
{
return (evt != null) ? evt.altKey : false;
},
/**
* Function: isControlDown
*
* Returns true if the control key is pressed for the given event.
*/
isControlDown: function(evt)
{
return (evt != null) ? evt.ctrlKey : false;
},
/**
* Function: isMetaDown
*
* Returns true if the meta key is pressed for the given event.
*/
isMetaDown: function(evt)
{
return (evt != null) ? evt.metaKey : false;
},
/**
* Function: getMainEvent
*
* Returns the touch or mouse event that contains the mouse coordinates.
*/
getMainEvent: function(e)
{
if ((e.type == 'touchstart' || e.type == 'touchmove') && e.touches != null && e.touches[0] != null)
{
e = e.touches[0];
}
else if (e.type == 'touchend' && e.changedTouches != null && e.changedTouches[0] != null)
{
e = e.changedTouches[0];
}
return e;
},
/**
* Function: getClientX
*
* Returns true if the meta key is pressed for the given event.
*/
getClientX: function(e)
{
return mxEvent.getMainEvent(e).clientX;
},
/**
* Function: getClientY
*
* Returns true if the meta key is pressed for the given event.
*/
getClientY: function(e)
{
return mxEvent.getMainEvent(e).clientY;
},
/**
* Function: consume
*
* Consumes the given event.
*
* Parameters:
*
* evt - Native event to be consumed.
* preventDefault - Optional boolean to prevent the default for the event.
* Default is true.
* stopPropagation - Option boolean to stop event propagation. Default is
* true.
*/
consume: function(evt, preventDefault, stopPropagation)
{
preventDefault = (preventDefault != null) ? preventDefault : true;
stopPropagation = (stopPropagation != null) ? stopPropagation : true;
if (preventDefault)
{
if (evt.preventDefault)
{
if (stopPropagation)
{
evt.stopPropagation();
}
evt.preventDefault();
}
else if (stopPropagation)
{
evt.cancelBubble = true;
}
}
// Opera
evt.isConsumed = true;
// Other browsers
if (!evt.preventDefault)
{
evt.returnValue = false;
}
},
//
// Special handles in mouse events
//
/**
* Variable: LABEL_HANDLE
*
* Index for the label handle in an mxMouseEvent. This should be a negative
* value that does not interfere with any possible handle indices. Default
* is -1.
*/
LABEL_HANDLE: -1,
/**
* Variable: ROTATION_HANDLE
*
* Index for the rotation handle in an mxMouseEvent. This should be a
* negative value that does not interfere with any possible handle indices.
* Default is -2.
*/
ROTATION_HANDLE: -2,
/**
* Variable: CUSTOM_HANDLE
*
* Start index for the custom handles in an mxMouseEvent. This should be a
* negative value and is the start index which is decremented for each
* custom handle. Default is -100.
*/
CUSTOM_HANDLE: -100,
/**
* Variable: VIRTUAL_HANDLE
*
* Start index for the virtual handles in an mxMouseEvent. This should be a
* negative value and is the start index which is decremented for each
* virtual handle. Default is -100000. This assumes that there are no more
* than VIRTUAL_HANDLE - CUSTOM_HANDLE custom handles.
*
*/
VIRTUAL_HANDLE: -100000,
//
// Event names
//
/**
* Variable: MOUSE_DOWN
*
* Specifies the event name for mouseDown.
*/
MOUSE_DOWN: 'mouseDown',
/**
* Variable: MOUSE_MOVE
*
* Specifies the event name for mouseMove.
*/
MOUSE_MOVE: 'mouseMove',
/**
* Variable: MOUSE_UP
*
* Specifies the event name for mouseUp.
*/
MOUSE_UP: 'mouseUp',
/**
* Variable: ACTIVATE
*
* Specifies the event name for activate.
*/
ACTIVATE: 'activate',
/**
* Variable: RESIZE_START
*
* Specifies the event name for resizeStart.
*/
RESIZE_START: 'resizeStart',
/**
* Variable: RESIZE
*
* Specifies the event name for resize.
*/
RESIZE: 'resize',
/**
* Variable: RESIZE_END
*
* Specifies the event name for resizeEnd.
*/
RESIZE_END: 'resizeEnd',
/**
* Variable: MOVE_START
*
* Specifies the event name for moveStart.
*/
MOVE_START: 'moveStart',
/**
* Variable: MOVE
*
* Specifies the event name for move.
*/
MOVE: 'move',
/**
* Variable: MOVE_END
*
* Specifies the event name for moveEnd.
*/
MOVE_END: 'moveEnd',
/**
* Variable: PAN_START
*
* Specifies the event name for panStart.
*/
PAN_START: 'panStart',
/**
* Variable: PAN
*
* Specifies the event name for pan.
*/
PAN: 'pan',
/**
* Variable: PAN_END
*
* Specifies the event name for panEnd.
*/
PAN_END: 'panEnd',
/**
* Variable: MINIMIZE
*
* Specifies the event name for minimize.
*/
MINIMIZE: 'minimize',
/**
* Variable: NORMALIZE
*
* Specifies the event name for normalize.
*/
NORMALIZE: 'normalize',
/**
* Variable: MAXIMIZE
*
* Specifies the event name for maximize.
*/
MAXIMIZE: 'maximize',
/**
* Variable: HIDE
*
* Specifies the event name for hide.
*/
HIDE: 'hide',
/**
* Variable: SHOW
*
* Specifies the event name for show.
*/
SHOW: 'show',
/**
* Variable: CLOSE
*
* Specifies the event name for close.
*/
CLOSE: 'close',
/**
* Variable: DESTROY
*
* Specifies the event name for destroy.
*/
DESTROY: 'destroy',
/**
* Variable: REFRESH
*
* Specifies the event name for refresh.
*/
REFRESH: 'refresh',
/**
* Variable: SIZE
*
* Specifies the event name for size.
*/
SIZE: 'size',
/**
* Variable: SELECT
*
* Specifies the event name for select.
*/
SELECT: 'select',
/**
* Variable: FIRED
*
* Specifies the event name for fired.
*/
FIRED: 'fired',
/**
* Variable: FIRE_MOUSE_EVENT
*
* Specifies the event name for fireMouseEvent.
*/
FIRE_MOUSE_EVENT: 'fireMouseEvent',
/**
* Variable: GESTURE
*
* Specifies the event name for gesture.
*/
GESTURE: 'gesture',
/**
* Variable: TAP_AND_HOLD
*
* Specifies the event name for tapAndHold.
*/
TAP_AND_HOLD: 'tapAndHold',
/**
* Variable: GET
*
* Specifies the event name for get.
*/
GET: 'get',
/**
* Variable: RECEIVE
*
* Specifies the event name for receive.
*/
RECEIVE: 'receive',
/**
* Variable: CONNECT
*
* Specifies the event name for connect.
*/
CONNECT: 'connect',
/**
* Variable: DISCONNECT
*
* Specifies the event name for disconnect.
*/
DISCONNECT: 'disconnect',
/**
* Variable: SUSPEND
*
* Specifies the event name for suspend.
*/
SUSPEND: 'suspend',
/**
* Variable: RESUME
*
* Specifies the event name for suspend.
*/
RESUME: 'resume',
/**
* Variable: MARK
*
* Specifies the event name for mark.
*/
MARK: 'mark',
/**
* Variable: ROOT
*
* Specifies the event name for root.
*/
ROOT: 'root',
/**
* Variable: POST
*
* Specifies the event name for post.
*/
POST: 'post',
/**
* Variable: OPEN
*
* Specifies the event name for open.
*/
OPEN: 'open',
/**
* Variable: SAVE
*
* Specifies the event name for open.
*/
SAVE: 'save',
/**
* Variable: BEFORE_ADD_VERTEX
*
* Specifies the event name for beforeAddVertex.
*/
BEFORE_ADD_VERTEX: 'beforeAddVertex',
/**
* Variable: ADD_VERTEX
*
* Specifies the event name for addVertex.
*/
ADD_VERTEX: 'addVertex',
/**
* Variable: AFTER_ADD_VERTEX
*
* Specifies the event name for afterAddVertex.
*/
AFTER_ADD_VERTEX: 'afterAddVertex',
/**
* Variable: DONE
*
* Specifies the event name for done.
*/
DONE: 'done',
/**
* Variable: EXECUTE
*
* Specifies the event name for execute.
*/
EXECUTE: 'execute',
/**
* Variable: EXECUTED
*
* Specifies the event name for executed.
*/
EXECUTED: 'executed',
/**
* Variable: BEGIN_UPDATE
*
* Specifies the event name for beginUpdate.
*/
BEGIN_UPDATE: 'beginUpdate',
/**
* Variable: START_EDIT
*
* Specifies the event name for startEdit.
*/
START_EDIT: 'startEdit',
/**
* Variable: END_UPDATE
*
* Specifies the event name for endUpdate.
*/
END_UPDATE: 'endUpdate',
/**
* Variable: END_EDIT
*
* Specifies the event name for endEdit.
*/
END_EDIT: 'endEdit',
/**
* Variable: BEFORE_UNDO
*
* Specifies the event name for beforeUndo.
*/
BEFORE_UNDO: 'beforeUndo',
/**
* Variable: UNDO
*
* Specifies the event name for undo.
*/
UNDO: 'undo',
/**
* Variable: REDO
*
* Specifies the event name for redo.
*/
REDO: 'redo',
/**
* Variable: CHANGE
*
* Specifies the event name for change.
*/
CHANGE: 'change',
/**
* Variable: NOTIFY
*
* Specifies the event name for notify.
*/
NOTIFY: 'notify',
/**
* Variable: LAYOUT_CELLS
*
* Specifies the event name for layoutCells.
*/
LAYOUT_CELLS: 'layoutCells',
/**
* Variable: CLICK
*
* Specifies the event name for click.
*/
CLICK: 'click',
/**
* Variable: SCALE
*
* Specifies the event name for scale.
*/
SCALE: 'scale',
/**
* Variable: TRANSLATE
*
* Specifies the event name for translate.
*/
TRANSLATE: 'translate',
/**
* Variable: SCALE_AND_TRANSLATE
*
* Specifies the event name for scaleAndTranslate.
*/
SCALE_AND_TRANSLATE: 'scaleAndTranslate',
/**
* Variable: UP
*
* Specifies the event name for up.
*/
UP: 'up',
/**
* Variable: DOWN
*
* Specifies the event name for down.
*/
DOWN: 'down',
/**
* Variable: ADD
*
* Specifies the event name for add.
*/
ADD: 'add',
/**
* Variable: REMOVE
*
* Specifies the event name for remove.
*/
REMOVE: 'remove',
/**
* Variable: CLEAR
*
* Specifies the event name for clear.
*/
CLEAR: 'clear',
/**
* Variable: ADD_CELLS
*
* Specifies the event name for addCells.
*/
ADD_CELLS: 'addCells',
/**
* Variable: CELLS_ADDED
*
* Specifies the event name for cellsAdded.
*/
CELLS_ADDED: 'cellsAdded',
/**
* Variable: MOVE_CELLS
*
* Specifies the event name for moveCells.
*/
MOVE_CELLS: 'moveCells',
/**
* Variable: CELLS_MOVED
*
* Specifies the event name for cellsMoved.
*/
CELLS_MOVED: 'cellsMoved',
/**
* Variable: RESIZE_CELLS
*
* Specifies the event name for resizeCells.
*/
RESIZE_CELLS: 'resizeCells',
/**
* Variable: CELLS_RESIZED
*
* Specifies the event name for cellsResized.
*/
CELLS_RESIZED: 'cellsResized',
/**
* Variable: TOGGLE_CELLS
*
* Specifies the event name for toggleCells.
*/
TOGGLE_CELLS: 'toggleCells',
/**
* Variable: CELLS_TOGGLED
*
* Specifies the event name for cellsToggled.
*/
CELLS_TOGGLED: 'cellsToggled',
/**
* Variable: ORDER_CELLS
*
* Specifies the event name for orderCells.
*/
ORDER_CELLS: 'orderCells',
/**
* Variable: CELLS_ORDERED
*
* Specifies the event name for cellsOrdered.
*/
CELLS_ORDERED: 'cellsOrdered',
/**
* Variable: REMOVE_CELLS
*
* Specifies the event name for removeCells.
*/
REMOVE_CELLS: 'removeCells',
/**
* Variable: CELLS_REMOVED
*
* Specifies the event name for cellsRemoved.
*/
CELLS_REMOVED: 'cellsRemoved',
/**
* Variable: GROUP_CELLS
*
* Specifies the event name for groupCells.
*/
GROUP_CELLS: 'groupCells',
/**
* Variable: UNGROUP_CELLS
*
* Specifies the event name for ungroupCells.
*/
UNGROUP_CELLS: 'ungroupCells',
/**
* Variable: REMOVE_CELLS_FROM_PARENT
*
* Specifies the event name for removeCellsFromParent.
*/
REMOVE_CELLS_FROM_PARENT: 'removeCellsFromParent',
/**
* Variable: FOLD_CELLS
*
* Specifies the event name for foldCells.
*/
FOLD_CELLS: 'foldCells',
/**
* Variable: CELLS_FOLDED
*
* Specifies the event name for cellsFolded.
*/
CELLS_FOLDED: 'cellsFolded',
/**
* Variable: ALIGN_CELLS
*
* Specifies the event name for alignCells.
*/
ALIGN_CELLS: 'alignCells',
/**
* Variable: LABEL_CHANGED
*
* Specifies the event name for labelChanged.
*/
LABEL_CHANGED: 'labelChanged',
/**
* Variable: CONNECT_CELL
*
* Specifies the event name for connectCell.
*/
CONNECT_CELL: 'connectCell',
/**
* Variable: CELL_CONNECTED
*
* Specifies the event name for cellConnected.
*/
CELL_CONNECTED: 'cellConnected',
/**
* Variable: SPLIT_EDGE
*
* Specifies the event name for splitEdge.
*/
SPLIT_EDGE: 'splitEdge',
/**
* Variable: FLIP_EDGE
*
* Specifies the event name for flipEdge.
*/
FLIP_EDGE: 'flipEdge',
/**
* Variable: START_EDITING
*
* Specifies the event name for startEditing.
*/
START_EDITING: 'startEditing',
/**
* Variable: EDITING_STARTED
*
* Specifies the event name for editingStarted.
*/
EDITING_STARTED: 'editingStarted',
/**
* Variable: EDITING_STOPPED
*
* Specifies the event name for editingStopped.
*/
EDITING_STOPPED: 'editingStopped',
/**
* Variable: ADD_OVERLAY
*
* Specifies the event name for addOverlay.
*/
ADD_OVERLAY: 'addOverlay',
/**
* Variable: REMOVE_OVERLAY
*
* Specifies the event name for removeOverlay.
*/
REMOVE_OVERLAY: 'removeOverlay',
/**
* Variable: UPDATE_CELL_SIZE
*
* Specifies the event name for updateCellSize.
*/
UPDATE_CELL_SIZE: 'updateCellSize',
/**
* Variable: ESCAPE
*
* Specifies the event name for escape.
*/
ESCAPE: 'escape',
/**
* Variable: DOUBLE_CLICK
*
* Specifies the event name for doubleClick.
*/
DOUBLE_CLICK: 'doubleClick',
/**
* Variable: START
*
* Specifies the event name for start.
*/
START: 'start',
/**
* Variable: RESET
*
* Specifies the event name for reset.
*/
RESET: 'reset'
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxXmlRequest
*
* XML HTTP request wrapper. See also: <mxUtils.get>, <mxUtils.post> and
* <mxUtils.load>. This class provides a cross-browser abstraction for Ajax
* requests.
*
* Encoding:
*
* For encoding parameter values, the built-in encodeURIComponent JavaScript
* method must be used. For automatic encoding of post data in <mxEditor> the
* <mxEditor.escapePostData> switch can be set to true (default). The encoding
* will be carried out using the conte type of the page. That is, the page
* containting the editor should contain a meta tag in the header, eg.
* <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
*
* Example:
*
* (code)
* var onload = function(req)
* {
* mxUtils.alert(req.getDocumentElement());
* }
*
* var onerror = function(req)
* {
* mxUtils.alert('Error');
* }
* new mxXmlRequest(url, 'key=value').send(onload, onerror);
* (end)
*
* Sends an asynchronous POST request to the specified URL.
*
* Example:
*
* (code)
* var req = new mxXmlRequest(url, 'key=value', 'POST', false);
* req.send();
* mxUtils.alert(req.getDocumentElement());
* (end)
*
* Sends a synchronous POST request to the specified URL.
*
* Example:
*
* (code)
* var encoder = new mxCodec();
* var result = encoder.encode(graph.getModel());
* var xml = encodeURIComponent(mxUtils.getXml(result));
* new mxXmlRequest(url, 'xml='+xml).send();
* (end)
*
* Sends an encoded graph model to the specified URL using xml as the
* parameter name. The parameter can then be retrieved in C# as follows:
*
* (code)
* string xml = HttpUtility.UrlDecode(context.Request.Params["xml"]);
* (end)
*
* Or in Java as follows:
*
* (code)
* String xml = URLDecoder.decode(request.getParameter("xml"), "UTF-8").replace("\n", "&#xa;");
* (end)
*
* Note that the linefeeds should only be replaced if the XML is
* processed in Java, for example when creating an image.
*
* Constructor: mxXmlRequest
*
* Constructs an XML HTTP request.
*
* Parameters:
*
* url - Target URL of the request.
* params - Form encoded parameters to send with a POST request.
* method - String that specifies the request method. Possible values are
* POST and GET. Default is POST.
* async - Boolean specifying if an asynchronous request should be used.
* Default is true.
* username - String specifying the username to be used for the request.
* password - String specifying the password to be used for the request.
*/
function mxXmlRequest(url, params, method, async, username, password)
{
this.url = url;
this.params = params;
this.method = method || 'POST';
this.async = (async != null) ? async : true;
this.username = username;
this.password = password;
};
/**
* Variable: url
*
* Holds the target URL of the request.
*/
mxXmlRequest.prototype.url = null;
/**
* Variable: params
*
* Holds the form encoded data for the POST request.
*/
mxXmlRequest.prototype.params = null;
/**
* Variable: method
*
* Specifies the request method. Possible values are POST and GET. Default
* is POST.
*/
mxXmlRequest.prototype.method = null;
/**
* Variable: async
*
* Boolean indicating if the request is asynchronous.
*/
mxXmlRequest.prototype.async = null;
/**
* Variable: binary
*
* Boolean indicating if the request is binary. This option is ignored in IE.
* In all other browsers the requested mime type is set to
* text/plain; charset=x-user-defined. Default is false.
*/
mxXmlRequest.prototype.binary = false;
/**
* Variable: withCredentials
*
* Specifies if withCredentials should be used in HTML5-compliant browsers. Default is
* false.
*/
mxXmlRequest.prototype.withCredentials = false;
/**
* Variable: username
*
* Specifies the username to be used for authentication.
*/
mxXmlRequest.prototype.username = null;
/**
* Variable: password
*
* Specifies the password to be used for authentication.
*/
mxXmlRequest.prototype.password = null;
/**
* Variable: request
*
* Holds the inner, browser-specific request object.
*/
mxXmlRequest.prototype.request = null;
/**
* Variable: decodeSimulateValues
*
* Specifies if request values should be decoded as URIs before setting the
* textarea value in <simulate>. Defaults to false for backwards compatibility,
* to avoid another decode on the server this should be set to true.
*/
mxXmlRequest.prototype.decodeSimulateValues = false;
/**
* Function: isBinary
*
* Returns <binary>.
*/
mxXmlRequest.prototype.isBinary = function()
{
return this.binary;
};
/**
* Function: setBinary
*
* Sets <binary>.
*/
mxXmlRequest.prototype.setBinary = function(value)
{
this.binary = value;
};
/**
* Function: getText
*
* Returns the response as a string.
*/
mxXmlRequest.prototype.getText = function()
{
return this.request.responseText;
};
/**
* Function: isReady
*
* Returns true if the response is ready.
*/
mxXmlRequest.prototype.isReady = function()
{
return this.request.readyState == 4;
};
/**
* Function: getDocumentElement
*
* Returns the document element of the response XML document.
*/
mxXmlRequest.prototype.getDocumentElement = function()
{
var doc = this.getXml();
if (doc != null)
{
return doc.documentElement;
}
return null;
};
/**
* Function: getXml
*
* Returns the response as an XML document. Use <getDocumentElement> to get
* the document element of the XML document.
*/
mxXmlRequest.prototype.getXml = function()
{
var xml = this.request.responseXML;
// Handles missing response headers in IE, the first condition handles
// the case where responseXML is there, but using its nodes leads to
// type errors in the mxCellCodec when putting the nodes into a new
// document. This happens in IE9 standards mode and with XML user
// objects only, as they are used directly as values in cells.
if (document.documentMode >= 9 || xml == null || xml.documentElement == null)
{
xml = mxUtils.parseXml(this.request.responseText);
}
return xml;
};
/**
* Function: getText
*
* Returns the response as a string.
*/
mxXmlRequest.prototype.getText = function()
{
return this.request.responseText;
};
/**
* Function: getStatus
*
* Returns the status as a number, eg. 404 for "Not found" or 200 for "OK".
* Note: The NS_ERROR_NOT_AVAILABLE for invalid responses cannot be cought.
*/
mxXmlRequest.prototype.getStatus = function()
{
return this.request.status;
};
/**
* Function: create
*
* Creates and returns the inner <request> object.
*/
mxXmlRequest.prototype.create = function()
{
if (window.XMLHttpRequest)
{
return function()
{
var req = new XMLHttpRequest();
// TODO: Check for overrideMimeType required here?
if (this.isBinary() && req.overrideMimeType)
{
req.overrideMimeType('text/plain; charset=x-user-defined');
}
return req;
};
}
else if (typeof(ActiveXObject) != 'undefined')
{
return function()
{
// TODO: Implement binary option
return new ActiveXObject('Microsoft.XMLHTTP');
};
}
}();
/**
* Function: send
*
* Send the <request> to the target URL using the specified functions to
* process the response asychronously.
*
* Note: Due to technical limitations, onerror is currently ignored.
*
* Parameters:
*
* onload - Function to be invoked if a successful response was received.
* onerror - Function to be called on any error.
* timeout - Optional timeout in ms before calling ontimeout.
* ontimeout - Optional function to execute on timeout.
*/
mxXmlRequest.prototype.send = function(onload, onerror, timeout, ontimeout)
{
this.request = this.create();
if (this.request != null)
{
if (onload != null)
{
this.request.onreadystatechange = mxUtils.bind(this, function()
{
if (this.isReady())
{
onload(this);
this.request.onreadystatechaange = null;
}
});
}
this.request.open(this.method, this.url, this.async,
this.username, this.password);
this.setRequestHeaders(this.request, this.params);
if (window.XMLHttpRequest && this.withCredentials)
{
this.request.withCredentials = 'true';
}
if (!mxClient.IS_QUIRKS && (document.documentMode == null || document.documentMode > 9) &&
window.XMLHttpRequest && timeout != null && ontimeout != null)
{
this.request.timeout = timeout;
this.request.ontimeout = ontimeout;
}
this.request.send(this.params);
}
};
/**
* Function: setRequestHeaders
*
* Sets the headers for the given request and parameters. This sets the
* content-type to application/x-www-form-urlencoded if any params exist.
*
* Example:
*
* (code)
* request.setRequestHeaders = function(request, params)
* {
* if (params != null)
* {
* request.setRequestHeader('Content-Type',
* 'multipart/form-data');
* request.setRequestHeader('Content-Length',
* params.length);
* }
* };
* (end)
*
* Use the code above before calling <send> if you require a
* multipart/form-data request.
*/
mxXmlRequest.prototype.setRequestHeaders = function(request, params)
{
if (params != null)
{
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
};
/**
* Function: simulate
*
* Creates and posts a request to the given target URL using a dynamically
* created form inside the given document.
*
* Parameters:
*
* docs - Document that contains the form element.
* target - Target to send the form result to.
*/
mxXmlRequest.prototype.simulate = function(doc, target)
{
doc = doc || document;
var old = null;
if (doc == document)
{
old = window.onbeforeunload;
window.onbeforeunload = null;
}
var form = doc.createElement('form');
form.setAttribute('method', this.method);
form.setAttribute('action', this.url);
if (target != null)
{
form.setAttribute('target', target);
}
form.style.display = 'none';
form.style.visibility = 'hidden';
var pars = (this.params.indexOf('&') > 0) ?
this.params.split('&') :
this.params.split();
// Adds the parameters as textareas to the form
for (var i=0; i<pars.length; i++)
{
var pos = pars[i].indexOf('=');
if (pos > 0)
{
var name = pars[i].substring(0, pos);
var value = pars[i].substring(pos+1);
if (this.decodeSimulateValues)
{
value = decodeURIComponent(value);
}
var textarea = doc.createElement('textarea');
textarea.setAttribute('wrap', 'off');
textarea.setAttribute('name', name);
mxUtils.write(textarea, value);
form.appendChild(textarea);
}
}
doc.body.appendChild(form);
form.submit();
if (form.parentNode != null)
{
form.parentNode.removeChild(form);
}
if (old != null)
{
window.onbeforeunload = old;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxClipboard =
{
/**
* Class: mxClipboard
*
* Singleton that implements a clipboard for graph cells.
*
* Example:
*
* (code)
* mxClipboard.copy(graph);
* mxClipboard.paste(graph2);
* (end)
*
* This copies the selection cells from the graph to the clipboard and
* pastes them into graph2.
*
* For fine-grained control of the clipboard data the <mxGraph.canExportCell>
* and <mxGraph.canImportCell> functions can be overridden.
*
* To restore previous parents for pasted cells, the implementation for
* <copy> and <paste> can be changed as follows.
*
* (code)
* mxClipboard.copy = function(graph, cells)
* {
* cells = cells || graph.getSelectionCells();
* var result = graph.getExportableCells(cells);
*
* mxClipboard.parents = new Object();
*
* for (var i = 0; i < result.length; i++)
* {
* mxClipboard.parents[i] = graph.model.getParent(cells[i]);
* }
*
* mxClipboard.insertCount = 1;
* mxClipboard.setCells(graph.cloneCells(result));
*
* return result;
* };
*
* mxClipboard.paste = function(graph)
* {
* if (!mxClipboard.isEmpty())
* {
* var cells = graph.getImportableCells(mxClipboard.getCells());
* var delta = mxClipboard.insertCount * mxClipboard.STEPSIZE;
* var parent = graph.getDefaultParent();
*
* graph.model.beginUpdate();
* try
* {
* for (var i = 0; i < cells.length; i++)
* {
* var tmp = (mxClipboard.parents != null && graph.model.contains(mxClipboard.parents[i])) ?
* mxClipboard.parents[i] : parent;
* cells[i] = graph.importCells([cells[i]], delta, delta, tmp)[0];
* }
* }
* finally
* {
* graph.model.endUpdate();
* }
*
* // Increments the counter and selects the inserted cells
* mxClipboard.insertCount++;
* graph.setSelectionCells(cells);
* }
* };
* (end)
*
* Variable: STEPSIZE
*
* Defines the step size to offset the cells after each paste operation.
* Default is 10.
*/
STEPSIZE: 10,
/**
* Variable: insertCount
*
* Counts the number of times the clipboard data has been inserted.
*/
insertCount: 1,
/**
* Variable: cells
*
* Holds the array of <mxCells> currently in the clipboard.
*/
cells: null,
/**
* Function: setCells
*
* Sets the cells in the clipboard. Fires a <mxEvent.CHANGE> event.
*/
setCells: function(cells)
{
mxClipboard.cells = cells;
},
/**
* Function: getCells
*
* Returns the cells in the clipboard.
*/
getCells: function()
{
return mxClipboard.cells;
},
/**
* Function: isEmpty
*
* Returns true if the clipboard currently has not data stored.
*/
isEmpty: function()
{
return mxClipboard.getCells() == null;
},
/**
* Function: cut
*
* Cuts the given array of <mxCells> from the specified graph.
* If cells is null then the selection cells of the graph will
* be used. Returns the cells that have been cut from the graph.
*
* Parameters:
*
* graph - <mxGraph> that contains the cells to be cut.
* cells - Optional array of <mxCells> to be cut.
*/
cut: function(graph, cells)
{
cells = mxClipboard.copy(graph, cells);
mxClipboard.insertCount = 0;
mxClipboard.removeCells(graph, cells);
return cells;
},
/**
* Function: removeCells
*
* Hook to remove the given cells from the given graph after
* a cut operation.
*
* Parameters:
*
* graph - <mxGraph> that contains the cells to be cut.
* cells - Array of <mxCells> to be cut.
*/
removeCells: function(graph, cells)
{
graph.removeCells(cells);
},
/**
* Function: copy
*
* Copies the given array of <mxCells> from the specified
* graph to <cells>. Returns the original array of cells that has
* been cloned. Descendants of cells in the array are ignored.
*
* Parameters:
*
* graph - <mxGraph> that contains the cells to be copied.
* cells - Optional array of <mxCells> to be copied.
*/
copy: function(graph, cells)
{
cells = cells || graph.getSelectionCells();
var result = graph.getExportableCells(graph.model.getTopmostCells(cells));
mxClipboard.insertCount = 1;
mxClipboard.setCells(graph.cloneCells(result));
return result;
},
/**
* Function: paste
*
* Pastes the <cells> into the specified graph restoring
* the relation to <parents>, if possible. If the parents
* are no longer in the graph or invisible then the
* cells are added to the graph's default or into the
* swimlane under the cell's new location if one exists.
* The cells are added to the graph using <mxGraph.importCells>
* and returned.
*
* Parameters:
*
* graph - <mxGraph> to paste the <cells> into.
*/
paste: function(graph)
{
var cells = null;
if (!mxClipboard.isEmpty())
{
cells = graph.getImportableCells(mxClipboard.getCells());
var delta = mxClipboard.insertCount * mxClipboard.STEPSIZE;
var parent = graph.getDefaultParent();
cells = graph.importCells(cells, delta, delta, parent);
// Increments the counter and selects the inserted cells
mxClipboard.insertCount++;
graph.setSelectionCells(cells);
}
return cells;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxWindow
*
* Basic window inside a document.
*
* Examples:
*
* Creating a simple window.
*
* (code)
* var tb = document.createElement('div');
* var wnd = new mxWindow('Title', tb, 100, 100, 200, 200, true, true);
* wnd.setVisible(true);
* (end)
*
* Creating a window that contains an iframe.
*
* (code)
* var frame = document.createElement('iframe');
* frame.setAttribute('width', '192px');
* frame.setAttribute('height', '172px');
* frame.setAttribute('src', 'http://www.example.com/');
* frame.style.backgroundColor = 'white';
*
* var w = document.body.clientWidth;
* var h = (document.body.clientHeight || document.documentElement.clientHeight);
* var wnd = new mxWindow('Title', frame, (w-200)/2, (h-200)/3, 200, 200);
* wnd.setVisible(true);
* (end)
*
* To limit the movement of a window, eg. to keep it from being moved beyond
* the top, left corner the following method can be overridden (recommended):
*
* (code)
* wnd.setLocation = function(x, y)
* {
* x = Math.max(0, x);
* y = Math.max(0, y);
* mxWindow.prototype.setLocation.apply(this, arguments);
* };
* (end)
*
* Or the following event handler can be used:
*
* (code)
* wnd.addListener(mxEvent.MOVE, function(e)
* {
* wnd.setLocation(Math.max(0, wnd.getX()), Math.max(0, wnd.getY()));
* });
* (end)
*
* To keep a window inside the current window:
*
* (code)
* mxEvent.addListener(window, 'resize', mxUtils.bind(this, function()
* {
* var iw = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
* var ih = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
*
* var x = this.window.getX();
* var y = this.window.getY();
*
* if (x + this.window.table.clientWidth > iw)
* {
* x = Math.max(0, iw - this.window.table.clientWidth);
* }
*
* if (y + this.window.table.clientHeight > ih)
* {
* y = Math.max(0, ih - this.window.table.clientHeight);
* }
*
* if (this.window.getX() != x || this.window.getY() != y)
* {
* this.window.setLocation(x, y);
* }
* }));
* (end)
*
* Event: mxEvent.MOVE_START
*
* Fires before the window is moved. The <code>event</code> property contains
* the corresponding mouse event.
*
* Event: mxEvent.MOVE
*
* Fires while the window is being moved. The <code>event</code> property
* contains the corresponding mouse event.
*
* Event: mxEvent.MOVE_END
*
* Fires after the window is moved. The <code>event</code> property contains
* the corresponding mouse event.
*
* Event: mxEvent.RESIZE_START
*
* Fires before the window is resized. The <code>event</code> property contains
* the corresponding mouse event.
*
* Event: mxEvent.RESIZE
*
* Fires while the window is being resized. The <code>event</code> property
* contains the corresponding mouse event.
*
* Event: mxEvent.RESIZE_END
*
* Fires after the window is resized. The <code>event</code> property contains
* the corresponding mouse event.
*
* Event: mxEvent.MAXIMIZE
*
* Fires after the window is maximized. The <code>event</code> property
* contains the corresponding mouse event.
*
* Event: mxEvent.MINIMIZE
*
* Fires after the window is minimized. The <code>event</code> property
* contains the corresponding mouse event.
*
* Event: mxEvent.NORMALIZE
*
* Fires after the window is normalized, that is, it returned from
* maximized or minimized state. The <code>event</code> property contains the
* corresponding mouse event.
*
* Event: mxEvent.ACTIVATE
*
* Fires after a window is activated. The <code>previousWindow</code> property
* contains the previous window. The event sender is the active window.
*
* Event: mxEvent.SHOW
*
* Fires after the window is shown. This event has no properties.
*
* Event: mxEvent.HIDE
*
* Fires after the window is hidden. This event has no properties.
*
* Event: mxEvent.CLOSE
*
* Fires before the window is closed. The <code>event</code> property contains
* the corresponding mouse event.
*
* Event: mxEvent.DESTROY
*
* Fires before the window is destroyed. This event has no properties.
*
* Constructor: mxWindow
*
* Constructs a new window with the given dimension and title to display
* the specified content. The window elements use the given style as a
* prefix for the classnames of the respective window elements, namely,
* the window title and window pane. The respective postfixes are appended
* to the given stylename as follows:
*
* style - Base style for the window.
* style+Title - Style for the window title.
* style+Pane - Style for the window pane.
*
* The default value for style is mxWindow, resulting in the following
* classnames for the window elements: mxWindow, mxWindowTitle and
* mxWindowPane.
*
* If replaceNode is given then the window replaces the given DOM node in
* the document.
*
* Parameters:
*
* title - String that represents the title of the new window.
* content - DOM node that is used as the window content.
* x - X-coordinate of the window location.
* y - Y-coordinate of the window location.
* width - Width of the window.
* height - Optional height of the window. Default is to match the height
* of the content at the specified width.
* minimizable - Optional boolean indicating if the window is minimizable.
* Default is true.
* movable - Optional boolean indicating if the window is movable. Default
* is true.
* replaceNode - Optional DOM node that the window should replace.
* style - Optional base classname for the window elements. Default is
* mxWindow.
*/
function mxWindow(title, content, x, y, width, height, minimizable, movable, replaceNode, style)
{
if (content != null)
{
minimizable = (minimizable != null) ? minimizable : true;
this.content = content;
this.init(x, y, width, height, style);
this.installMaximizeHandler();
this.installMinimizeHandler();
this.installCloseHandler();
this.setMinimizable(minimizable);
this.setTitle(title);
if (movable == null || movable)
{
this.installMoveHandler();
}
if (replaceNode != null && replaceNode.parentNode != null)
{
replaceNode.parentNode.replaceChild(this.div, replaceNode);
}
else
{
document.body.appendChild(this.div);
}
}
};
/**
* Extends mxEventSource.
*/
mxWindow.prototype = new mxEventSource();
mxWindow.prototype.constructor = mxWindow;
/**
* Variable: closeImage
*
* URL of the image to be used for the close icon in the titlebar.
*/
mxWindow.prototype.closeImage = mxClient.imageBasePath + '/close.gif';
/**
* Variable: minimizeImage
*
* URL of the image to be used for the minimize icon in the titlebar.
*/
mxWindow.prototype.minimizeImage = mxClient.imageBasePath + '/minimize.gif';
/**
* Variable: normalizeImage
*
* URL of the image to be used for the normalize icon in the titlebar.
*/
mxWindow.prototype.normalizeImage = mxClient.imageBasePath + '/normalize.gif';
/**
* Variable: maximizeImage
*
* URL of the image to be used for the maximize icon in the titlebar.
*/
mxWindow.prototype.maximizeImage = mxClient.imageBasePath + '/maximize.gif';
/**
* Variable: normalizeImage
*
* URL of the image to be used for the resize icon.
*/
mxWindow.prototype.resizeImage = mxClient.imageBasePath + '/resize.gif';
/**
* Variable: visible
*
* Boolean flag that represents the visible state of the window.
*/
mxWindow.prototype.visible = false;
/**
* Variable: minimumSize
*
* <mxRectangle> that specifies the minimum width and height of the window.
* Default is (50, 40).
*/
mxWindow.prototype.minimumSize = new mxRectangle(0, 0, 50, 40);
/**
* Variable: destroyOnClose
*
* Specifies if the window should be destroyed when it is closed. If this
* is false then the window is hidden using <setVisible>. Default is true.
*/
mxWindow.prototype.destroyOnClose = true;
/**
* Variable: contentHeightCorrection
*
* Defines the correction factor for computing the height of the contentWrapper.
* Default is 6 for IE 7/8 standards mode and 2 for all other browsers and modes.
*/
mxWindow.prototype.contentHeightCorrection = (document.documentMode == 8 || document.documentMode == 7) ? 6 : 2;
/**
* Variable: title
*
* Reference to the DOM node (TD) that contains the title.
*/
mxWindow.prototype.title = null;
/**
* Variable: content
*
* Reference to the DOM node that represents the window content.
*/
mxWindow.prototype.content = null;
/**
* Function: init
*
* Initializes the DOM tree that represents the window.
*/
mxWindow.prototype.init = function(x, y, width, height, style)
{
style = (style != null) ? style : 'mxWindow';
this.div = document.createElement('div');
this.div.className = style;
this.div.style.left = x + 'px';
this.div.style.top = y + 'px';
this.table = document.createElement('table');
this.table.className = style;
// Disables built-in pan and zoom in IE10 and later
if (mxClient.IS_POINTER)
{
this.div.style.touchAction = 'none';
}
// Workaround for table size problems in FF
if (width != null)
{
if (!mxClient.IS_QUIRKS)
{
this.div.style.width = width + 'px';
}
this.table.style.width = width + 'px';
}
if (height != null)
{
if (!mxClient.IS_QUIRKS)
{
this.div.style.height = height + 'px';
}
this.table.style.height = height + 'px';
}
// Creates title row
var tbody = document.createElement('tbody');
var tr = document.createElement('tr');
this.title = document.createElement('td');
this.title.className = style + 'Title';
this.buttons = document.createElement('div');
this.buttons.style.position = 'absolute';
this.buttons.style.display = 'inline-block';
this.buttons.style.right = '4px';
this.buttons.style.top = '5px';
this.title.appendChild(this.buttons);
tr.appendChild(this.title);
tbody.appendChild(tr);
// Creates content row and table cell
tr = document.createElement('tr');
this.td = document.createElement('td');
this.td.className = style + 'Pane';
if (document.documentMode == 7)
{
this.td.style.height = '100%';
}
this.contentWrapper = document.createElement('div');
this.contentWrapper.className = style + 'Pane';
this.contentWrapper.style.width = '100%';
this.contentWrapper.appendChild(this.content);
// Workaround for div around div restricts height
// of inner div if outerdiv has hidden overflow
if (mxClient.IS_QUIRKS || this.content.nodeName.toUpperCase() != 'DIV')
{
this.contentWrapper.style.height = '100%';
}
// Puts all content into the DOM
this.td.appendChild(this.contentWrapper);
tr.appendChild(this.td);
tbody.appendChild(tr);
this.table.appendChild(tbody);
this.div.appendChild(this.table);
// Puts the window on top of other windows when clicked
var activator = mxUtils.bind(this, function(evt)
{
this.activate();
});
mxEvent.addGestureListeners(this.title, activator);
mxEvent.addGestureListeners(this.table, activator);
this.hide();
};
/**
* Function: setTitle
*
* Sets the window title to the given string. HTML markup inside the title
* will be escaped.
*/
mxWindow.prototype.setTitle = function(title)
{
// Removes all text content nodes (normally just one)
var child = this.title.firstChild;
while (child != null)
{
var next = child.nextSibling;
if (child.nodeType == mxConstants.NODETYPE_TEXT)
{
child.parentNode.removeChild(child);
}
child = next;
}
mxUtils.write(this.title, title || '');
this.title.appendChild(this.buttons);
};
/**
* Function: setScrollable
*
* Sets if the window contents should be scrollable.
*/
mxWindow.prototype.setScrollable = function(scrollable)
{
// Workaround for hang in Presto 2.5.22 (Opera 10.5)
if (navigator.userAgent.indexOf('Presto/2.5') < 0)
{
if (scrollable)
{
this.contentWrapper.style.overflow = 'auto';
}
else
{
this.contentWrapper.style.overflow = 'hidden';
}
}
};
/**
* Function: activate
*
* Puts the window on top of all other windows.
*/
mxWindow.prototype.activate = function()
{
if (mxWindow.activeWindow != this)
{
var style = mxUtils.getCurrentStyle(this.getElement());
var index = (style != null) ? style.zIndex : 3;
if (mxWindow.activeWindow)
{
var elt = mxWindow.activeWindow.getElement();
if (elt != null && elt.style != null)
{
elt.style.zIndex = index;
}
}
var previousWindow = mxWindow.activeWindow;
this.getElement().style.zIndex = parseInt(index) + 1;
mxWindow.activeWindow = this;
this.fireEvent(new mxEventObject(mxEvent.ACTIVATE, 'previousWindow', previousWindow));
}
};
/**
* Function: getElement
*
* Returuns the outermost DOM node that makes up the window.
*/
mxWindow.prototype.getElement = function()
{
return this.div;
};
/**
* Function: fit
*
* Makes sure the window is inside the client area of the window.
*/
mxWindow.prototype.fit = function()
{
mxUtils.fit(this.div);
};
/**
* Function: isResizable
*
* Returns true if the window is resizable.
*/
mxWindow.prototype.isResizable = function()
{
if (this.resize != null)
{
return this.resize.style.display != 'none';
}
return false;
};
/**
* Function: setResizable
*
* Sets if the window should be resizable. To avoid interference with some
* built-in features of IE10 and later, the use of the following code is
* recommended if there are resizable <mxWindow>s in the page:
*
* (code)
* if (mxClient.IS_POINTER)
* {
* document.body.style.msTouchAction = 'none';
* }
* (end)
*/
mxWindow.prototype.setResizable = function(resizable)
{
if (resizable)
{
if (this.resize == null)
{
this.resize = document.createElement('img');
this.resize.style.position = 'absolute';
this.resize.style.bottom = '2px';
this.resize.style.right = '2px';
this.resize.setAttribute('src', this.resizeImage);
this.resize.style.cursor = 'nw-resize';
var startX = null;
var startY = null;
var width = null;
var height = null;
var start = mxUtils.bind(this, function(evt)
{
// LATER: pointerdown starting on border of resize does start
// the drag operation but does not fire consecutive events via
// one of the listeners below (does pan instead).
// Workaround: document.body.style.msTouchAction = 'none'
this.activate();
startX = mxEvent.getClientX(evt);
startY = mxEvent.getClientY(evt);
width = this.div.offsetWidth;
height = this.div.offsetHeight;
mxEvent.addGestureListeners(document, null, dragHandler, dropHandler);
this.fireEvent(new mxEventObject(mxEvent.RESIZE_START, 'event', evt));
mxEvent.consume(evt);
});
// Adds a temporary pair of listeners to intercept
// the gesture event in the document
var dragHandler = mxUtils.bind(this, function(evt)
{
if (startX != null && startY != null)
{
var dx = mxEvent.getClientX(evt) - startX;
var dy = mxEvent.getClientY(evt) - startY;
this.setSize(width + dx, height + dy);
this.fireEvent(new mxEventObject(mxEvent.RESIZE, 'event', evt));
mxEvent.consume(evt);
}
});
var dropHandler = mxUtils.bind(this, function(evt)
{
if (startX != null && startY != null)
{
startX = null;
startY = null;
mxEvent.removeGestureListeners(document, null, dragHandler, dropHandler);
this.fireEvent(new mxEventObject(mxEvent.RESIZE_END, 'event', evt));
mxEvent.consume(evt);
}
});
mxEvent.addGestureListeners(this.resize, start, dragHandler, dropHandler);
this.div.appendChild(this.resize);
}
else
{
this.resize.style.display = 'inline';
}
}
else if (this.resize != null)
{
this.resize.style.display = 'none';
}
};
/**
* Function: setSize
*
* Sets the size of the window.
*/
mxWindow.prototype.setSize = function(width, height)
{
width = Math.max(this.minimumSize.width, width);
height = Math.max(this.minimumSize.height, height);
// Workaround for table size problems in FF
if (!mxClient.IS_QUIRKS)
{
this.div.style.width = width + 'px';
this.div.style.height = height + 'px';
}
this.table.style.width = width + 'px';
this.table.style.height = height + 'px';
if (!mxClient.IS_QUIRKS)
{
this.contentWrapper.style.height = (this.div.offsetHeight -
this.title.offsetHeight - this.contentHeightCorrection) + 'px';
}
};
/**
* Function: setMinimizable
*
* Sets if the window is minimizable.
*/
mxWindow.prototype.setMinimizable = function(minimizable)
{
this.minimize.style.display = (minimizable) ? '' : 'none';
};
/**
* Function: getMinimumSize
*
* Returns an <mxRectangle> that specifies the size for the minimized window.
* A width or height of 0 means keep the existing width or height. This
* implementation returns the height of the window title and keeps the width.
*/
mxWindow.prototype.getMinimumSize = function()
{
return new mxRectangle(0, 0, 0, this.title.offsetHeight);
};
/**
* Function: installMinimizeHandler
*
* Installs the event listeners required for minimizing the window.
*/
mxWindow.prototype.installMinimizeHandler = function()
{
this.minimize = document.createElement('img');
this.minimize.setAttribute('src', this.minimizeImage);
this.minimize.setAttribute('title', 'Minimize');
this.minimize.style.cursor = 'pointer';
this.minimize.style.marginLeft = '2px';
this.minimize.style.display = 'none';
this.buttons.appendChild(this.minimize);
var minimized = false;
var maxDisplay = null;
var height = null;
var funct = mxUtils.bind(this, function(evt)
{
this.activate();
if (!minimized)
{
minimized = true;
this.minimize.setAttribute('src', this.normalizeImage);
this.minimize.setAttribute('title', 'Normalize');
this.contentWrapper.style.display = 'none';
maxDisplay = this.maximize.style.display;
this.maximize.style.display = 'none';
height = this.table.style.height;
var minSize = this.getMinimumSize();
if (minSize.height > 0)
{
if (!mxClient.IS_QUIRKS)
{
this.div.style.height = minSize.height + 'px';
}
this.table.style.height = minSize.height + 'px';
}
if (minSize.width > 0)
{
if (!mxClient.IS_QUIRKS)
{
this.div.style.width = minSize.width + 'px';
}
this.table.style.width = minSize.width + 'px';
}
if (this.resize != null)
{
this.resize.style.visibility = 'hidden';
}
this.fireEvent(new mxEventObject(mxEvent.MINIMIZE, 'event', evt));
}
else
{
minimized = false;
this.minimize.setAttribute('src', this.minimizeImage);
this.minimize.setAttribute('title', 'Minimize');
this.contentWrapper.style.display = ''; // default
this.maximize.style.display = maxDisplay;
if (!mxClient.IS_QUIRKS)
{
this.div.style.height = height;
}
this.table.style.height = height;
if (this.resize != null)
{
this.resize.style.visibility = '';
}
this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt));
}
mxEvent.consume(evt);
});
mxEvent.addGestureListeners(this.minimize, funct);
};
/**
* Function: setMaximizable
*
* Sets if the window is maximizable.
*/
mxWindow.prototype.setMaximizable = function(maximizable)
{
this.maximize.style.display = (maximizable) ? '' : 'none';
};
/**
* Function: installMaximizeHandler
*
* Installs the event listeners required for maximizing the window.
*/
mxWindow.prototype.installMaximizeHandler = function()
{
this.maximize = document.createElement('img');
this.maximize.setAttribute('src', this.maximizeImage);
this.maximize.setAttribute('title', 'Maximize');
this.maximize.style.cursor = 'default';
this.maximize.style.marginLeft = '2px';
this.maximize.style.cursor = 'pointer';
this.maximize.style.display = 'none';
this.buttons.appendChild(this.maximize);
var maximized = false;
var x = null;
var y = null;
var height = null;
var width = null;
var minDisplay = null;
var funct = mxUtils.bind(this, function(evt)
{
this.activate();
if (this.maximize.style.display != 'none')
{
if (!maximized)
{
maximized = true;
this.maximize.setAttribute('src', this.normalizeImage);
this.maximize.setAttribute('title', 'Normalize');
this.contentWrapper.style.display = '';
minDisplay = this.minimize.style.display;
this.minimize.style.display = 'none';
// Saves window state
x = parseInt(this.div.style.left);
y = parseInt(this.div.style.top);
height = this.table.style.height;
width = this.table.style.width;
this.div.style.left = '0px';
this.div.style.top = '0px';
var docHeight = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0);
if (!mxClient.IS_QUIRKS)
{
this.div.style.width = (document.body.clientWidth - 2) + 'px';
this.div.style.height = (docHeight - 2) + 'px';
}
this.table.style.width = (document.body.clientWidth - 2) + 'px';
this.table.style.height = (docHeight - 2) + 'px';
if (this.resize != null)
{
this.resize.style.visibility = 'hidden';
}
if (!mxClient.IS_QUIRKS)
{
var style = mxUtils.getCurrentStyle(this.contentWrapper);
if (style.overflow == 'auto' || this.resize != null)
{
this.contentWrapper.style.height = (this.div.offsetHeight -
this.title.offsetHeight - this.contentHeightCorrection) + 'px';
}
}
this.fireEvent(new mxEventObject(mxEvent.MAXIMIZE, 'event', evt));
}
else
{
maximized = false;
this.maximize.setAttribute('src', this.maximizeImage);
this.maximize.setAttribute('title', 'Maximize');
this.contentWrapper.style.display = '';
this.minimize.style.display = minDisplay;
// Restores window state
this.div.style.left = x+'px';
this.div.style.top = y+'px';
if (!mxClient.IS_QUIRKS)
{
this.div.style.height = height;
this.div.style.width = width;
var style = mxUtils.getCurrentStyle(this.contentWrapper);
if (style.overflow == 'auto' || this.resize != null)
{
this.contentWrapper.style.height = (this.div.offsetHeight -
this.title.offsetHeight - this.contentHeightCorrection) + 'px';
}
}
this.table.style.height = height;
this.table.style.width = width;
if (this.resize != null)
{
this.resize.style.visibility = '';
}
this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt));
}
mxEvent.consume(evt);
}
});
mxEvent.addGestureListeners(this.maximize, funct);
mxEvent.addListener(this.title, 'dblclick', funct);
};
/**
* Function: installMoveHandler
*
* Installs the event listeners required for moving the window.
*/
mxWindow.prototype.installMoveHandler = function()
{
this.title.style.cursor = 'move';
mxEvent.addGestureListeners(this.title,
mxUtils.bind(this, function(evt)
{
var startX = mxEvent.getClientX(evt);
var startY = mxEvent.getClientY(evt);
var x = this.getX();
var y = this.getY();
// Adds a temporary pair of listeners to intercept
// the gesture event in the document
var dragHandler = mxUtils.bind(this, function(evt)
{
var dx = mxEvent.getClientX(evt) - startX;
var dy = mxEvent.getClientY(evt) - startY;
this.setLocation(x + dx, y + dy);
this.fireEvent(new mxEventObject(mxEvent.MOVE, 'event', evt));
mxEvent.consume(evt);
});
var dropHandler = mxUtils.bind(this, function(evt)
{
mxEvent.removeGestureListeners(document, null, dragHandler, dropHandler);
this.fireEvent(new mxEventObject(mxEvent.MOVE_END, 'event', evt));
mxEvent.consume(evt);
});
mxEvent.addGestureListeners(document, null, dragHandler, dropHandler);
this.fireEvent(new mxEventObject(mxEvent.MOVE_START, 'event', evt));
mxEvent.consume(evt);
}));
// Disables built-in pan and zoom in IE10 and later
if (mxClient.IS_POINTER)
{
this.title.style.touchAction = 'none';
}
};
/**
* Function: setLocation
*
* Sets the upper, left corner of the window.
*/
mxWindow.prototype.setLocation = function(x, y)
{
this.div.style.left = x + 'px';
this.div.style.top = y + 'px';
};
/**
* Function: getX
*
* Returns the current position on the x-axis.
*/
mxWindow.prototype.getX = function()
{
return parseInt(this.div.style.left);
};
/**
* Function: getY
*
* Returns the current position on the y-axis.
*/
mxWindow.prototype.getY = function()
{
return parseInt(this.div.style.top);
};
/**
* Function: installCloseHandler
*
* Adds the <closeImage> as a new image node in <closeImg> and installs the
* <close> event.
*/
mxWindow.prototype.installCloseHandler = function()
{
this.closeImg = document.createElement('img');
this.closeImg.setAttribute('src', this.closeImage);
this.closeImg.setAttribute('title', 'Close');
this.closeImg.style.marginLeft = '2px';
this.closeImg.style.cursor = 'pointer';
this.closeImg.style.display = 'none';
this.buttons.appendChild(this.closeImg);
mxEvent.addGestureListeners(this.closeImg,
mxUtils.bind(this, function(evt)
{
this.fireEvent(new mxEventObject(mxEvent.CLOSE, 'event', evt));
if (this.destroyOnClose)
{
this.destroy();
}
else
{
this.setVisible(false);
}
mxEvent.consume(evt);
}));
};
/**
* Function: setImage
*
* Sets the image associated with the window.
*
* Parameters:
*
* image - URL of the image to be used.
*/
mxWindow.prototype.setImage = function(image)
{
this.image = document.createElement('img');
this.image.setAttribute('src', image);
this.image.setAttribute('align', 'left');
this.image.style.marginRight = '4px';
this.image.style.marginLeft = '0px';
this.image.style.marginTop = '-2px';
this.title.insertBefore(this.image, this.title.firstChild);
};
/**
* Function: setClosable
*
* Sets the image associated with the window.
*
* Parameters:
*
* closable - Boolean specifying if the window should be closable.
*/
mxWindow.prototype.setClosable = function(closable)
{
this.closeImg.style.display = (closable) ? '' : 'none';
};
/**
* Function: isVisible
*
* Returns true if the window is visible.
*/
mxWindow.prototype.isVisible = function()
{
if (this.div != null)
{
return this.div.style.display != 'none';
}
return false;
};
/**
* Function: setVisible
*
* Shows or hides the window depending on the given flag.
*
* Parameters:
*
* visible - Boolean indicating if the window should be made visible.
*/
mxWindow.prototype.setVisible = function(visible)
{
if (this.div != null && this.isVisible() != visible)
{
if (visible)
{
this.show();
}
else
{
this.hide();
}
}
};
/**
* Function: show
*
* Shows the window.
*/
mxWindow.prototype.show = function()
{
this.div.style.display = '';
this.activate();
var style = mxUtils.getCurrentStyle(this.contentWrapper);
if (!mxClient.IS_QUIRKS && (style.overflow == 'auto' || this.resize != null) &&
this.contentWrapper.style.display != 'none')
{
this.contentWrapper.style.height = (this.div.offsetHeight -
this.title.offsetHeight - this.contentHeightCorrection) + 'px';
}
this.fireEvent(new mxEventObject(mxEvent.SHOW));
};
/**
* Function: hide
*
* Hides the window.
*/
mxWindow.prototype.hide = function()
{
this.div.style.display = 'none';
this.fireEvent(new mxEventObject(mxEvent.HIDE));
};
/**
* Function: destroy
*
* Destroys the window and removes all associated resources. Fires a
* <destroy> event prior to destroying the window.
*/
mxWindow.prototype.destroy = function()
{
this.fireEvent(new mxEventObject(mxEvent.DESTROY));
if (this.div != null)
{
mxEvent.release(this.div);
this.div.parentNode.removeChild(this.div);
this.div = null;
}
this.title = null;
this.content = null;
this.contentWrapper = null;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxForm
*
* A simple class for creating HTML forms.
*
* Constructor: mxForm
*
* Creates a HTML table using the specified classname.
*/
function mxForm(className)
{
this.table = document.createElement('table');
this.table.className = className;
this.body = document.createElement('tbody');
this.table.appendChild(this.body);
};
/**
* Variable: table
*
* Holds the DOM node that represents the table.
*/
mxForm.prototype.table = null;
/**
* Variable: body
*
* Holds the DOM node that represents the tbody (table body). New rows
* can be added to this object using DOM API.
*/
mxForm.prototype.body = false;
/**
* Function: getTable
*
* Returns the table that contains this form.
*/
mxForm.prototype.getTable = function()
{
return this.table;
};
/**
* Function: addButtons
*
* Helper method to add an OK and Cancel button using the respective
* functions.
*/
mxForm.prototype.addButtons = function(okFunct, cancelFunct)
{
var tr = document.createElement('tr');
var td = document.createElement('td');
tr.appendChild(td);
td = document.createElement('td');
// Adds the ok button
var button = document.createElement('button');
mxUtils.write(button, mxResources.get('ok') || 'OK');
td.appendChild(button);
mxEvent.addListener(button, 'click', function()
{
okFunct();
});
// Adds the cancel button
button = document.createElement('button');
mxUtils.write(button, mxResources.get('cancel') || 'Cancel');
td.appendChild(button);
mxEvent.addListener(button, 'click', function()
{
cancelFunct();
});
tr.appendChild(td);
this.body.appendChild(tr);
};
/**
* Function: addText
*
* Adds an input for the given name, type and value and returns it.
*/
mxForm.prototype.addText = function(name, value, type)
{
var input = document.createElement('input');
input.setAttribute('type', type || 'text');
input.value = value;
return this.addField(name, input);
};
/**
* Function: addCheckbox
*
* Adds a checkbox for the given name and value and returns the textfield.
*/
mxForm.prototype.addCheckbox = function(name, value)
{
var input = document.createElement('input');
input.setAttribute('type', 'checkbox');
this.addField(name, input);
// IE can only change the checked value if the input is inside the DOM
if (value)
{
input.checked = true;
}
return input;
};
/**
* Function: addTextarea
*
* Adds a textarea for the given name and value and returns the textarea.
*/
mxForm.prototype.addTextarea = function(name, value, rows)
{
var input = document.createElement('textarea');
if (mxClient.IS_NS)
{
rows--;
}
input.setAttribute('rows', rows || 2);
input.value = value;
return this.addField(name, input);
};
/**
* Function: addCombo
*
* Adds a combo for the given name and returns the combo.
*/
mxForm.prototype.addCombo = function(name, isMultiSelect, size)
{
var select = document.createElement('select');
if (size != null)
{
select.setAttribute('size', size);
}
if (isMultiSelect)
{
select.setAttribute('multiple', 'true');
}
return this.addField(name, select);
};
/**
* Function: addOption
*
* Adds an option for the given label to the specified combo.
*/
mxForm.prototype.addOption = function(combo, label, value, isSelected)
{
var option = document.createElement('option');
mxUtils.writeln(option, label);
option.setAttribute('value', value);
if (isSelected)
{
option.setAttribute('selected', isSelected);
}
combo.appendChild(option);
};
/**
* Function: addField
*
* Adds a new row with the name and the input field in two columns and
* returns the given input.
*/
mxForm.prototype.addField = function(name, input)
{
var tr = document.createElement('tr');
var td = document.createElement('td');
mxUtils.write(td, name);
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(input);
tr.appendChild(td);
this.body.appendChild(tr);
return input;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxImage
*
* Encapsulates the URL, width and height of an image.
*
* Constructor: mxImage
*
* Constructs a new image.
*/
function mxImage(src, width, height)
{
this.src = src;
this.width = width;
this.height = height;
};
/**
* Variable: src
*
* String that specifies the URL of the image.
*/
mxImage.prototype.src = null;
/**
* Variable: width
*
* Integer that specifies the width of the image.
*/
mxImage.prototype.width = null;
/**
* Variable: height
*
* Integer that specifies the height of the image.
*/
mxImage.prototype.height = null;
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDivResizer
*
* Maintains the size of a div element in Internet Explorer. This is a
* workaround for the right and bottom style being ignored in IE.
*
* If you need a div to cover the scrollwidth and -height of a document,
* then you can use this class as follows:
*
* (code)
* var resizer = new mxDivResizer(background);
* resizer.getDocumentHeight = function()
* {
* return document.body.scrollHeight;
* }
* resizer.getDocumentWidth = function()
* {
* return document.body.scrollWidth;
* }
* resizer.resize();
* (end)
*
* Constructor: mxDivResizer
*
* Constructs an object that maintains the size of a div
* element when the window is being resized. This is only
* required for Internet Explorer as it ignores the respective
* stylesheet information for DIV elements.
*
* Parameters:
*
* div - Reference to the DOM node whose size should be maintained.
* container - Optional Container that contains the div. Default is the
* window.
*/
function mxDivResizer(div, container)
{
if (div.nodeName.toLowerCase() == 'div')
{
if (container == null)
{
container = window;
}
this.div = div;
var style = mxUtils.getCurrentStyle(div);
if (style != null)
{
this.resizeWidth = style.width == 'auto';
this.resizeHeight = style.height == 'auto';
}
mxEvent.addListener(container, 'resize',
mxUtils.bind(this, function(evt)
{
if (!this.handlingResize)
{
this.handlingResize = true;
this.resize();
this.handlingResize = false;
}
})
);
this.resize();
}
};
/**
* Function: resizeWidth
*
* Boolean specifying if the width should be updated.
*/
mxDivResizer.prototype.resizeWidth = true;
/**
* Function: resizeHeight
*
* Boolean specifying if the height should be updated.
*/
mxDivResizer.prototype.resizeHeight = true;
/**
* Function: handlingResize
*
* Boolean specifying if the width should be updated.
*/
mxDivResizer.prototype.handlingResize = false;
/**
* Function: resize
*
* Updates the style of the DIV after the window has been resized.
*/
mxDivResizer.prototype.resize = function()
{
var w = this.getDocumentWidth();
var h = this.getDocumentHeight();
var l = parseInt(this.div.style.left);
var r = parseInt(this.div.style.right);
var t = parseInt(this.div.style.top);
var b = parseInt(this.div.style.bottom);
if (this.resizeWidth &&
!isNaN(l) &&
!isNaN(r) &&
l >= 0 &&
r >= 0 &&
w - r - l > 0)
{
this.div.style.width = (w - r - l)+'px';
}
if (this.resizeHeight &&
!isNaN(t) &&
!isNaN(b) &&
t >= 0 &&
b >= 0 &&
h - t - b > 0)
{
this.div.style.height = (h - t - b)+'px';
}
};
/**
* Function: getDocumentWidth
*
* Hook for subclassers to return the width of the document (without
* scrollbars).
*/
mxDivResizer.prototype.getDocumentWidth = function()
{
return document.body.clientWidth;
};
/**
* Function: getDocumentHeight
*
* Hook for subclassers to return the height of the document (without
* scrollbars).
*/
mxDivResizer.prototype.getDocumentHeight = function()
{
return document.body.clientHeight;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDragSource
*
* Wrapper to create a drag source from a DOM element so that the element can
* be dragged over a graph and dropped into the graph as a new cell.
*
* Problem is that in the dropHandler the current preview location is not
* available, so the preview and the dropHandler must match.
*
* Constructor: mxDragSource
*
* Constructs a new drag source for the given element.
*/
function mxDragSource(element, dropHandler)
{
this.element = element;
this.dropHandler = dropHandler;
// Handles a drag gesture on the element
mxEvent.addGestureListeners(element, mxUtils.bind(this, function(evt)
{
this.mouseDown(evt);
}));
// Prevents native drag and drop
mxEvent.addListener(element, 'dragstart', function(evt)
{
mxEvent.consume(evt);
});
this.eventConsumer = function(sender, evt)
{
var evtName = evt.getProperty('eventName');
var me = evt.getProperty('event');
if (evtName != mxEvent.MOUSE_DOWN)
{
me.consume();
}
};
};
/**
* Variable: element
*
* Reference to the DOM node which was made draggable.
*/
mxDragSource.prototype.element = null;
/**
* Variable: dropHandler
*
* Holds the DOM node that is used to represent the drag preview. If this is
* null then the source element will be cloned and used for the drag preview.
*/
mxDragSource.prototype.dropHandler = null;
/**
* Variable: dragOffset
*
* <mxPoint> that specifies the offset of the <dragElement>. Default is null.
*/
mxDragSource.prototype.dragOffset = null;
/**
* Variable: dragElement
*
* Holds the DOM node that is used to represent the drag preview. If this is
* null then the source element will be cloned and used for the drag preview.
*/
mxDragSource.prototype.dragElement = null;
/**
* Variable: previewElement
*
* Optional <mxRectangle> that specifies the unscaled size of the preview.
*/
mxDragSource.prototype.previewElement = null;
/**
* Variable: enabled
*
* Specifies if this drag source is enabled. Default is true.
*/
mxDragSource.prototype.enabled = true;
/**
* Variable: currentGraph
*
* Reference to the <mxGraph> that is the current drop target.
*/
mxDragSource.prototype.currentGraph = null;
/**
* Variable: currentDropTarget
*
* Holds the current drop target under the mouse.
*/
mxDragSource.prototype.currentDropTarget = null;
/**
* Variable: currentPoint
*
* Holds the current drop location.
*/
mxDragSource.prototype.currentPoint = null;
/**
* Variable: currentGuide
*
* Holds an <mxGuide> for the <currentGraph> if <dragPreview> is not null.
*/
mxDragSource.prototype.currentGuide = null;
/**
* Variable: currentGuide
*
* Holds an <mxGuide> for the <currentGraph> if <dragPreview> is not null.
*/
mxDragSource.prototype.currentHighlight = null;
/**
* Variable: autoscroll
*
* Specifies if the graph should scroll automatically. Default is true.
*/
mxDragSource.prototype.autoscroll = true;
/**
* Variable: guidesEnabled
*
* Specifies if <mxGuide> should be enabled. Default is true.
*/
mxDragSource.prototype.guidesEnabled = true;
/**
* Variable: gridEnabled
*
* Specifies if the grid should be allowed. Default is true.
*/
mxDragSource.prototype.gridEnabled = true;
/**
* Variable: highlightDropTargets
*
* Specifies if drop targets should be highlighted. Default is true.
*/
mxDragSource.prototype.highlightDropTargets = true;
/**
* Variable: dragElementZIndex
*
* ZIndex for the drag element. Default is 100.
*/
mxDragSource.prototype.dragElementZIndex = 100;
/**
* Variable: dragElementOpacity
*
* Opacity of the drag element in %. Default is 70.
*/
mxDragSource.prototype.dragElementOpacity = 70;
/**
* Variable: checkEventSource
*
* Whether the event source should be checked in <graphContainerEvent>. Default
* is true.
*/
mxDragSource.prototype.checkEventSource = true;
/**
* Function: isEnabled
*
* Returns <enabled>.
*/
mxDragSource.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Sets <enabled>.
*/
mxDragSource.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: isGuidesEnabled
*
* Returns <guidesEnabled>.
*/
mxDragSource.prototype.isGuidesEnabled = function()
{
return this.guidesEnabled;
};
/**
* Function: setGuidesEnabled
*
* Sets <guidesEnabled>.
*/
mxDragSource.prototype.setGuidesEnabled = function(value)
{
this.guidesEnabled = value;
};
/**
* Function: isGridEnabled
*
* Returns <gridEnabled>.
*/
mxDragSource.prototype.isGridEnabled = function()
{
return this.gridEnabled;
};
/**
* Function: setGridEnabled
*
* Sets <gridEnabled>.
*/
mxDragSource.prototype.setGridEnabled = function(value)
{
this.gridEnabled = value;
};
/**
* Function: getGraphForEvent
*
* Returns the graph for the given mouse event. This implementation returns
* null.
*/
mxDragSource.prototype.getGraphForEvent = function(evt)
{
return null;
};
/**
* Function: getDropTarget
*
* Returns the drop target for the given graph and coordinates. This
* implementation uses <mxGraph.getCellAt>.
*/
mxDragSource.prototype.getDropTarget = function(graph, x, y, evt)
{
return graph.getCellAt(x, y);
};
/**
* Function: createDragElement
*
* Creates and returns a clone of the <dragElementPrototype> or the <element>
* if the former is not defined.
*/
mxDragSource.prototype.createDragElement = function(evt)
{
return this.element.cloneNode(true);
};
/**
* Function: createPreviewElement
*
* Creates and returns an element which can be used as a preview in the given
* graph.
*/
mxDragSource.prototype.createPreviewElement = function(graph)
{
return null;
};
/**
* Function: isActive
*
* Returns true if this drag source is active.
*/
mxDragSource.prototype.isActive = function()
{
return this.mouseMoveHandler != null;
};
/**
* Function: reset
*
* Stops and removes everything and restores the state of the object.
*/
mxDragSource.prototype.reset = function()
{
if (this.currentGraph != null)
{
this.dragExit(this.currentGraph);
this.currentGraph = null;
}
this.removeDragElement();
this.removeListeners();
this.stopDrag();
};
/**
* Function: mouseDown
*
* Returns the drop target for the given graph and coordinates. This
* implementation uses <mxGraph.getCellAt>.
*
* To ignore popup menu events for a drag source, this function can be
* overridden as follows.
*
* (code)
* var mouseDown = dragSource.mouseDown;
*
* dragSource.mouseDown = function(evt)
* {
* if (!mxEvent.isPopupTrigger(evt))
* {
* mouseDown.apply(this, arguments);
* }
* };
* (end)
*/
mxDragSource.prototype.mouseDown = function(evt)
{
if (this.enabled && !mxEvent.isConsumed(evt) && this.mouseMoveHandler == null)
{
this.startDrag(evt);
this.mouseMoveHandler = mxUtils.bind(this, this.mouseMove);
this.mouseUpHandler = mxUtils.bind(this, this.mouseUp);
mxEvent.addGestureListeners(document, null, this.mouseMoveHandler, this.mouseUpHandler);
if (mxClient.IS_TOUCH && !mxEvent.isMouseEvent(evt))
{
this.eventSource = mxEvent.getSource(evt);
mxEvent.addGestureListeners(this.eventSource, null, this.mouseMoveHandler, this.mouseUpHandler);
}
}
};
/**
* Function: startDrag
*
* Creates the <dragElement> using <createDragElement>.
*/
mxDragSource.prototype.startDrag = function(evt)
{
this.dragElement = this.createDragElement(evt);
this.dragElement.style.position = 'absolute';
this.dragElement.style.zIndex = this.dragElementZIndex;
mxUtils.setOpacity(this.dragElement, this.dragElementOpacity);
if (this.checkEventSource && mxClient.IS_SVG)
{
this.dragElement.style.pointerEvents = 'none';
}
};
/**
* Function: stopDrag
*
* Invokes <removeDragElement>.
*/
mxDragSource.prototype.stopDrag = function()
{
// LATER: This used to have a mouse event. If that is still needed we need to add another
// final call to the DnD protocol to add a cleanup step in the case of escape press, which
// is not associated with a mouse event and which currently calles this method.
this.removeDragElement();
};
/**
* Function: removeDragElement
*
* Removes and destroys the <dragElement>.
*/
mxDragSource.prototype.removeDragElement = function()
{
if (this.dragElement != null)
{
if (this.dragElement.parentNode != null)
{
this.dragElement.parentNode.removeChild(this.dragElement);
}
this.dragElement = null;
}
};
/**
* Function: getElementForEvent
*
* Returns the topmost element under the given event.
*/
mxDragSource.prototype.getElementForEvent = function(evt)
{
return ((mxEvent.isTouchEvent(evt) || mxEvent.isPenEvent(evt)) ?
document.elementFromPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)) :
mxEvent.getSource(evt));
};
/**
* Function: graphContainsEvent
*
* Returns true if the given graph contains the given event.
*/
mxDragSource.prototype.graphContainsEvent = function(graph, evt)
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
var offset = mxUtils.getOffset(graph.container);
var origin = mxUtils.getScrollOrigin();
var elt = this.getElementForEvent(evt);
if (this.checkEventSource)
{
while (elt != null && elt != graph.container)
{
elt = elt.parentNode;
}
}
// Checks if event is inside the bounds of the graph container
return elt != null && x >= offset.x - origin.x && y >= offset.y - origin.y &&
x <= offset.x - origin.x + graph.container.offsetWidth &&
y <= offset.y - origin.y + graph.container.offsetHeight;
};
/**
* Function: mouseMove
*
* Gets the graph for the given event using <getGraphForEvent>, updates the
* <currentGraph>, calling <dragEnter> and <dragExit> on the new and old graph,
* respectively, and invokes <dragOver> if <currentGraph> is not null.
*/
mxDragSource.prototype.mouseMove = function(evt)
{
var graph = this.getGraphForEvent(evt);
// Checks if event is inside the bounds of the graph container
if (graph != null && !this.graphContainsEvent(graph, evt))
{
graph = null;
}
if (graph != this.currentGraph)
{
if (this.currentGraph != null)
{
this.dragExit(this.currentGraph, evt);
}
this.currentGraph = graph;
if (this.currentGraph != null)
{
this.dragEnter(this.currentGraph, evt);
}
}
if (this.currentGraph != null)
{
this.dragOver(this.currentGraph, evt);
}
if (this.dragElement != null && (this.previewElement == null || this.previewElement.style.visibility != 'visible'))
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
if (this.dragElement.parentNode == null)
{
document.body.appendChild(this.dragElement);
}
this.dragElement.style.visibility = 'visible';
if (this.dragOffset != null)
{
x += this.dragOffset.x;
y += this.dragOffset.y;
}
var offset = mxUtils.getDocumentScrollOrigin(document);
this.dragElement.style.left = (x + offset.x) + 'px';
this.dragElement.style.top = (y + offset.y) + 'px';
}
else if (this.dragElement != null)
{
this.dragElement.style.visibility = 'hidden';
}
mxEvent.consume(evt);
};
/**
* Function: mouseUp
*
* Processes the mouse up event and invokes <drop>, <dragExit> and <stopDrag>
* as required.
*/
mxDragSource.prototype.mouseUp = function(evt)
{
if (this.currentGraph != null)
{
if (this.currentPoint != null && (this.previewElement == null ||
this.previewElement.style.visibility != 'hidden'))
{
var scale = this.currentGraph.view.scale;
var tr = this.currentGraph.view.translate;
var x = this.currentPoint.x / scale - tr.x;
var y = this.currentPoint.y / scale - tr.y;
this.drop(this.currentGraph, evt, this.currentDropTarget, x, y);
}
this.dragExit(this.currentGraph);
this.currentGraph = null;
}
this.stopDrag();
this.removeListeners();
mxEvent.consume(evt);
};
/**
* Function: removeListeners
*
* Actives the given graph as a drop target.
*/
mxDragSource.prototype.removeListeners = function()
{
if (this.eventSource != null)
{
mxEvent.removeGestureListeners(this.eventSource, null, this.mouseMoveHandler, this.mouseUpHandler);
this.eventSource = null;
}
mxEvent.removeGestureListeners(document, null, this.mouseMoveHandler, this.mouseUpHandler);
this.mouseMoveHandler = null;
this.mouseUpHandler = null;
};
/**
* Function: dragEnter
*
* Actives the given graph as a drop target.
*/
mxDragSource.prototype.dragEnter = function(graph, evt)
{
graph.isMouseDown = true;
graph.isMouseTrigger = mxEvent.isMouseEvent(evt);
this.previewElement = this.createPreviewElement(graph);
if (this.previewElement != null && this.checkEventSource && mxClient.IS_SVG)
{
this.previewElement.style.pointerEvents = 'none';
}
// Guide is only needed if preview element is used
if (this.isGuidesEnabled() && this.previewElement != null)
{
this.currentGuide = new mxGuide(graph, graph.graphHandler.getGuideStates());
}
if (this.highlightDropTargets)
{
this.currentHighlight = new mxCellHighlight(graph, mxConstants.DROP_TARGET_COLOR);
}
// Consumes all events in the current graph before they are fired
graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.eventConsumer);
};
/**
* Function: dragExit
*
* Deactivates the given graph as a drop target.
*/
mxDragSource.prototype.dragExit = function(graph, evt)
{
this.currentDropTarget = null;
this.currentPoint = null;
graph.isMouseDown = false;
// Consumes all events in the current graph before they are fired
graph.removeListener(this.eventConsumer);
if (this.previewElement != null)
{
if (this.previewElement.parentNode != null)
{
this.previewElement.parentNode.removeChild(this.previewElement);
}
this.previewElement = null;
}
if (this.currentGuide != null)
{
this.currentGuide.destroy();
this.currentGuide = null;
}
if (this.currentHighlight != null)
{
this.currentHighlight.destroy();
this.currentHighlight = null;
}
};
/**
* Function: dragOver
*
* Implements autoscroll, updates the <currentPoint>, highlights any drop
* targets and updates the preview.
*/
mxDragSource.prototype.dragOver = function(graph, evt)
{
var offset = mxUtils.getOffset(graph.container);
var origin = mxUtils.getScrollOrigin(graph.container);
var x = mxEvent.getClientX(evt) - offset.x + origin.x - graph.panDx;
var y = mxEvent.getClientY(evt) - offset.y + origin.y - graph.panDy;
if (graph.autoScroll && (this.autoscroll == null || this.autoscroll))
{
graph.scrollPointToVisible(x, y, graph.autoExtend);
}
// Highlights the drop target under the mouse
if (this.currentHighlight != null && graph.isDropEnabled())
{
this.currentDropTarget = this.getDropTarget(graph, x, y, evt);
var state = graph.getView().getState(this.currentDropTarget);
this.currentHighlight.highlight(state);
}
// Updates the location of the preview
if (this.previewElement != null)
{
if (this.previewElement.parentNode == null)
{
graph.container.appendChild(this.previewElement);
this.previewElement.style.zIndex = '3';
this.previewElement.style.position = 'absolute';
}
var gridEnabled = this.isGridEnabled() && graph.isGridEnabledEvent(evt);
var hideGuide = true;
// Grid and guides
if (this.currentGuide != null && this.currentGuide.isEnabledForEvent(evt))
{
// LATER: HTML preview appears smaller than SVG preview
var w = parseInt(this.previewElement.style.width);
var h = parseInt(this.previewElement.style.height);
var bounds = new mxRectangle(0, 0, w, h);
var delta = new mxPoint(x, y);
delta = this.currentGuide.move(bounds, delta, gridEnabled, true);
hideGuide = false;
x = delta.x;
y = delta.y;
}
else if (gridEnabled)
{
var scale = graph.view.scale;
var tr = graph.view.translate;
var off = graph.gridSize / 2;
x = (graph.snap(x / scale - tr.x - off) + tr.x) * scale;
y = (graph.snap(y / scale - tr.y - off) + tr.y) * scale;
}
if (this.currentGuide != null && hideGuide)
{
this.currentGuide.hide();
}
if (this.previewOffset != null)
{
x += this.previewOffset.x;
y += this.previewOffset.y;
}
this.previewElement.style.left = Math.round(x) + 'px';
this.previewElement.style.top = Math.round(y) + 'px';
this.previewElement.style.visibility = 'visible';
}
this.currentPoint = new mxPoint(x, y);
};
/**
* Function: drop
*
* Returns the drop target for the given graph and coordinates. This
* implementation uses <mxGraph.getCellAt>.
*/
mxDragSource.prototype.drop = function(graph, evt, dropTarget, x, y)
{
this.dropHandler.apply(this, arguments);
// Had to move this to after the insert because it will
// affect the scrollbars of the window in IE to try and
// make the complete container visible.
// LATER: Should be made optional.
if (graph.container.style.visibility != 'hidden')
{
graph.container.focus();
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxToolbar
*
* Creates a toolbar inside a given DOM node. The toolbar may contain icons,
* buttons and combo boxes.
*
* Event: mxEvent.SELECT
*
* Fires when an item was selected in the toolbar. The <code>function</code>
* property contains the function that was selected in <selectMode>.
*
* Constructor: mxToolbar
*
* Constructs a toolbar in the specified container.
*
* Parameters:
*
* container - DOM node that contains the toolbar.
*/
function mxToolbar(container)
{
this.container = container;
};
/**
* Extends mxEventSource.
*/
mxToolbar.prototype = new mxEventSource();
mxToolbar.prototype.constructor = mxToolbar;
/**
* Variable: container
*
* Reference to the DOM nodes that contains the toolbar.
*/
mxToolbar.prototype.container = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxToolbar.prototype.enabled = true;
/**
* Variable: noReset
*
* Specifies if <resetMode> requires a forced flag of true for resetting
* the current mode in the toolbar. Default is false. This is set to true
* if the toolbar item is double clicked to avoid a reset after a single
* use of the item.
*/
mxToolbar.prototype.noReset = false;
/**
* Variable: updateDefaultMode
*
* Boolean indicating if the default mode should be the last selected
* switch mode or the first inserted switch mode. Default is true, that
* is the last selected switch mode is the default mode. The default mode
* is the mode to be selected after a reset of the toolbar. If this is
* false, then the default mode is the first inserted mode item regardless
* of what was last selected. Otherwise, the selected item after a reset is
* the previously selected item.
*/
mxToolbar.prototype.updateDefaultMode = true;
/**
* Function: addItem
*
* Adds the given function as an image with the specified title and icon
* and returns the new image node.
*
* Parameters:
*
* title - Optional string that is used as the tooltip.
* icon - Optional URL of the image to be used. If no URL is given, then a
* button is created.
* funct - Function to execute on a mouse click.
* pressedIcon - Optional URL of the pressed image. Default is a gray
* background.
* style - Optional style classname. Default is mxToolbarItem.
* factoryMethod - Optional factory method for popup menu, eg.
* function(menu, evt, cell) { menu.addItem('Hello, World!'); }
*/
mxToolbar.prototype.addItem = function(title, icon, funct, pressedIcon, style, factoryMethod)
{
var img = document.createElement((icon != null) ? 'img' : 'button');
var initialClassName = style || ((factoryMethod != null) ?
'mxToolbarMode' : 'mxToolbarItem');
img.className = initialClassName;
img.setAttribute('src', icon);
if (title != null)
{
if (icon != null)
{
img.setAttribute('title', title);
}
else
{
mxUtils.write(img, title);
}
}
this.container.appendChild(img);
// Invokes the function on a click on the toolbar item
if (funct != null)
{
mxEvent.addListener(img, 'click', funct);
if (mxClient.IS_TOUCH)
{
mxEvent.addListener(img, 'touchend', funct);
}
}
var mouseHandler = mxUtils.bind(this, function(evt)
{
if (pressedIcon != null)
{
img.setAttribute('src', icon);
}
else
{
img.style.backgroundColor = '';
}
});
// Highlights the toolbar item with a gray background
// while it is being clicked with the mouse
mxEvent.addGestureListeners(img, mxUtils.bind(this, function(evt)
{
if (pressedIcon != null)
{
img.setAttribute('src', pressedIcon);
}
else
{
img.style.backgroundColor = 'gray';
}
// Popup Menu
if (factoryMethod != null)
{
if (this.menu == null)
{
this.menu = new mxPopupMenu();
this.menu.init();
}
var last = this.currentImg;
if (this.menu.isMenuShowing())
{
this.menu.hideMenu();
}
if (last != img)
{
// Redirects factory method to local factory method
this.currentImg = img;
this.menu.factoryMethod = factoryMethod;
var point = new mxPoint(
img.offsetLeft,
img.offsetTop + img.offsetHeight);
this.menu.popup(point.x, point.y, null, evt);
// Sets and overrides to restore classname
if (this.menu.isMenuShowing())
{
img.className = initialClassName + 'Selected';
this.menu.hideMenu = function()
{
mxPopupMenu.prototype.hideMenu.apply(this);
img.className = initialClassName;
this.currentImg = null;
};
}
}
}
}), null, mouseHandler);
mxEvent.addListener(img, 'mouseout', mouseHandler);
return img;
};
/**
* Function: addCombo
*
* Adds and returns a new SELECT element using the given style. The element
* is placed inside a DIV with the mxToolbarComboContainer style classname.
*
* Parameters:
*
* style - Optional style classname. Default is mxToolbarCombo.
*/
mxToolbar.prototype.addCombo = function(style)
{
var div = document.createElement('div');
div.style.display = 'inline';
div.className = 'mxToolbarComboContainer';
var select = document.createElement('select');
select.className = style || 'mxToolbarCombo';
div.appendChild(select);
this.container.appendChild(div);
return select;
};
/**
* Function: addCombo
*
* Adds and returns a new SELECT element using the given title as the
* default element. The selection is reset to this element after each
* change.
*
* Parameters:
*
* title - String that specifies the title of the default element.
* style - Optional style classname. Default is mxToolbarCombo.
*/
mxToolbar.prototype.addActionCombo = function(title, style)
{
var select = document.createElement('select');
select.className = style || 'mxToolbarCombo';
this.addOption(select, title, null);
mxEvent.addListener(select, 'change', function(evt)
{
var value = select.options[select.selectedIndex];
select.selectedIndex = 0;
if (value.funct != null)
{
value.funct(evt);
}
});
this.container.appendChild(select);
return select;
};
/**
* Function: addOption
*
* Adds and returns a new OPTION element inside the given SELECT element.
* If the given value is a function then it is stored in the option's funct
* field.
*
* Parameters:
*
* combo - SELECT element that will contain the new entry.
* title - String that specifies the title of the option.
* value - Specifies the value associated with this option.
*/
mxToolbar.prototype.addOption = function(combo, title, value)
{
var option = document.createElement('option');
mxUtils.writeln(option, title);
if (typeof(value) == 'function')
{
option.funct = value;
}
else
{
option.setAttribute('value', value);
}
combo.appendChild(option);
return option;
};
/**
* Function: addSwitchMode
*
* Adds a new selectable item to the toolbar. Only one switch mode item may
* be selected at a time. The currently selected item is the default item
* after a reset of the toolbar.
*/
mxToolbar.prototype.addSwitchMode = function(title, icon, funct, pressedIcon, style)
{
var img = document.createElement('img');
img.initialClassName = style || 'mxToolbarMode';
img.className = img.initialClassName;
img.setAttribute('src', icon);
img.altIcon = pressedIcon;
if (title != null)
{
img.setAttribute('title', title);
}
mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt)
{
var tmp = this.selectedMode.altIcon;
if (tmp != null)
{
this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
this.selectedMode.setAttribute('src', tmp);
}
else
{
this.selectedMode.className = this.selectedMode.initialClassName;
}
if (this.updateDefaultMode)
{
this.defaultMode = img;
}
this.selectedMode = img;
var tmp = img.altIcon;
if (tmp != null)
{
img.altIcon = img.getAttribute('src');
img.setAttribute('src', tmp);
}
else
{
img.className = img.initialClassName+'Selected';
}
this.fireEvent(new mxEventObject(mxEvent.SELECT));
funct();
}));
this.container.appendChild(img);
if (this.defaultMode == null)
{
this.defaultMode = img;
// Function should fire only once so
// do not pass it with the select event
this.selectMode(img);
funct();
}
return img;
};
/**
* Function: addMode
*
* Adds a new item to the toolbar. The selection is typically reset after
* the item has been consumed, for example by adding a new vertex to the
* graph. The reset is not carried out if the item is double clicked.
*
* The function argument uses the following signature: funct(evt, cell) where
* evt is the native mouse event and cell is the cell under the mouse.
*/
mxToolbar.prototype.addMode = function(title, icon, funct, pressedIcon, style, toggle)
{
toggle = (toggle != null) ? toggle : true;
var img = document.createElement((icon != null) ? 'img' : 'button');
img.initialClassName = style || 'mxToolbarMode';
img.className = img.initialClassName;
img.setAttribute('src', icon);
img.altIcon = pressedIcon;
if (title != null)
{
img.setAttribute('title', title);
}
if (this.enabled && toggle)
{
mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt)
{
this.selectMode(img, funct);
this.noReset = false;
}));
mxEvent.addListener(img, 'dblclick', mxUtils.bind(this, function(evt)
{
this.selectMode(img, funct);
this.noReset = true;
}));
if (this.defaultMode == null)
{
this.defaultMode = img;
this.defaultFunction = funct;
this.selectMode(img, funct);
}
}
this.container.appendChild(img);
return img;
};
/**
* Function: selectMode
*
* Resets the state of the previously selected mode and displays the given
* DOM node as selected. This function fires a select event with the given
* function as a parameter.
*/
mxToolbar.prototype.selectMode = function(domNode, funct)
{
if (this.selectedMode != domNode)
{
if (this.selectedMode != null)
{
var tmp = this.selectedMode.altIcon;
if (tmp != null)
{
this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
this.selectedMode.setAttribute('src', tmp);
}
else
{
this.selectedMode.className = this.selectedMode.initialClassName;
}
}
this.selectedMode = domNode;
var tmp = this.selectedMode.altIcon;
if (tmp != null)
{
this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
this.selectedMode.setAttribute('src', tmp);
}
else
{
this.selectedMode.className = this.selectedMode.initialClassName+'Selected';
}
this.fireEvent(new mxEventObject(mxEvent.SELECT, "function", funct));
}
};
/**
* Function: resetMode
*
* Selects the default mode and resets the state of the previously selected
* mode.
*/
mxToolbar.prototype.resetMode = function(forced)
{
if ((forced || !this.noReset) && this.selectedMode != this.defaultMode)
{
// The last selected switch mode will be activated
// so the function was already executed and is
// no longer required here
this.selectMode(this.defaultMode, this.defaultFunction);
}
};
/**
* Function: addSeparator
*
* Adds the specifies image as a separator.
*
* Parameters:
*
* icon - URL of the separator icon.
*/
mxToolbar.prototype.addSeparator = function(icon)
{
return this.addItem(null, icon, null);
};
/**
* Function: addBreak
*
* Adds a break to the container.
*/
mxToolbar.prototype.addBreak = function()
{
mxUtils.br(this.container);
};
/**
* Function: addLine
*
* Adds a horizontal line to the container.
*/
mxToolbar.prototype.addLine = function()
{
var hr = document.createElement('hr');
hr.style.marginRight = '6px';
hr.setAttribute('size', '1');
this.container.appendChild(hr);
};
/**
* Function: destroy
*
* Removes the toolbar and all its associated resources.
*/
mxToolbar.prototype.destroy = function ()
{
mxEvent.release(this.container);
this.container = null;
this.defaultMode = null;
this.defaultFunction = null;
this.selectedMode = null;
if (this.menu != null)
{
this.menu.destroy();
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxUndoableEdit
*
* Implements a composite undoable edit. Here is an example for a custom change
* which gets executed via the model:
*
* (code)
* function CustomChange(model, name)
* {
* this.model = model;
* this.name = name;
* this.previous = name;
* };
*
* CustomChange.prototype.execute = function()
* {
* var tmp = this.model.name;
* this.model.name = this.previous;
* this.previous = tmp;
* };
*
* var name = prompt('Enter name');
* graph.model.execute(new CustomChange(graph.model, name));
* (end)
*
* Event: mxEvent.EXECUTED
*
* Fires between START_EDIT and END_EDIT after an atomic change was executed.
* The <code>change</code> property contains the change that was executed.
*
* Event: mxEvent.START_EDIT
*
* Fires before a set of changes will be executed in <undo> or <redo>.
* This event contains no properties.
*
* Event: mxEvent.END_EDIT
*
* Fires after a set of changeswas executed in <undo> or <redo>.
* This event contains no properties.
*
* Constructor: mxUndoableEdit
*
* Constructs a new undoable edit for the given source.
*/
function mxUndoableEdit(source, significant)
{
this.source = source;
this.changes = [];
this.significant = (significant != null) ? significant : true;
};
/**
* Variable: source
*
* Specifies the source of the edit.
*/
mxUndoableEdit.prototype.source = null;
/**
* Variable: changes
*
* Array that contains the changes that make up this edit. The changes are
* expected to either have an undo and redo function, or an execute
* function. Default is an empty array.
*/
mxUndoableEdit.prototype.changes = null;
/**
* Variable: significant
*
* Specifies if the undoable change is significant.
* Default is true.
*/
mxUndoableEdit.prototype.significant = null;
/**
* Variable: undone
*
* Specifies if this edit has been undone. Default is false.
*/
mxUndoableEdit.prototype.undone = false;
/**
* Variable: redone
*
* Specifies if this edit has been redone. Default is false.
*/
mxUndoableEdit.prototype.redone = false;
/**
* Function: isEmpty
*
* Returns true if the this edit contains no changes.
*/
mxUndoableEdit.prototype.isEmpty = function()
{
return this.changes.length == 0;
};
/**
* Function: isSignificant
*
* Returns <significant>.
*/
mxUndoableEdit.prototype.isSignificant = function()
{
return this.significant;
};
/**
* Function: add
*
* Adds the specified change to this edit. The change is an object that is
* expected to either have an undo and redo, or an execute function.
*/
mxUndoableEdit.prototype.add = function(change)
{
this.changes.push(change);
};
/**
* Function: notify
*
* Hook to notify any listeners of the changes after an <undo> or <redo>
* has been carried out. This implementation is empty.
*/
mxUndoableEdit.prototype.notify = function() { };
/**
* Function: die
*
* Hook to free resources after the edit has been removed from the command
* history. This implementation is empty.
*/
mxUndoableEdit.prototype.die = function() { };
/**
* Function: undo
*
* Undoes all changes in this edit.
*/
mxUndoableEdit.prototype.undo = function()
{
if (!this.undone)
{
this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));
var count = this.changes.length;
for (var i = count - 1; i >= 0; i--)
{
var change = this.changes[i];
if (change.execute != null)
{
change.execute();
}
else if (change.undo != null)
{
change.undo();
}
// New global executed event
this.source.fireEvent(new mxEventObject(mxEvent.EXECUTED, 'change', change));
}
this.undone = true;
this.redone = false;
this.source.fireEvent(new mxEventObject(mxEvent.END_EDIT));
}
this.notify();
};
/**
* Function: redo
*
* Redoes all changes in this edit.
*/
mxUndoableEdit.prototype.redo = function()
{
if (!this.redone)
{
this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));
var count = this.changes.length;
for (var i = 0; i < count; i++)
{
var change = this.changes[i];
if (change.execute != null)
{
change.execute();
}
else if (change.redo != null)
{
change.redo();
}
// New global executed event
this.source.fireEvent(new mxEventObject(mxEvent.EXECUTED, 'change', change));
}
this.undone = false;
this.redone = true;
this.source.fireEvent(new mxEventObject(mxEvent.END_EDIT));
}
this.notify();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxUndoManager
*
* Implements a command history. When changing the graph model, an
* <mxUndoableChange> object is created at the start of the transaction (when
* model.beginUpdate is called). All atomic changes are then added to this
* object until the last model.endUpdate call, at which point the
* <mxUndoableEdit> is dispatched in an event, and added to the history inside
* <mxUndoManager>. This is done by an event listener in
* <mxEditor.installUndoHandler>.
*
* Each atomic change of the model is represented by an object (eg.
* <mxRootChange>, <mxChildChange>, <mxTerminalChange> etc) which contains the
* complete undo information. The <mxUndoManager> also listens to the
* <mxGraphView> and stores it's changes to the current root as insignificant
* undoable changes, so that drilling (step into, step up) is undone.
*
* This means when you execute an atomic change on the model, then change the
* current root on the view and click undo, the change of the root will be
* undone together with the change of the model so that the display represents
* the state at which the model was changed. However, these changes are not
* transmitted for sharing as they do not represent a state change.
*
* Example:
*
* When adding an undo manager to a graph, make sure to add it
* to the model and the view as well to maintain a consistent
* display across multiple undo/redo steps.
*
* (code)
* var undoManager = new mxUndoManager();
* var listener = function(sender, evt)
* {
* undoManager.undoableEditHappened(evt.getProperty('edit'));
* };
* graph.getModel().addListener(mxEvent.UNDO, listener);
* graph.getView().addListener(mxEvent.UNDO, listener);
* (end)
*
* The code creates a function that informs the undoManager
* of an undoable edit and binds it to the undo event of
* <mxGraphModel> and <mxGraphView> using
* <mxEventSource.addListener>.
*
* Event: mxEvent.CLEAR
*
* Fires after <clear> was invoked. This event has no properties.
*
* Event: mxEvent.UNDO
*
* Fires afer a significant edit was undone in <undo>. The <code>edit</code>
* property contains the <mxUndoableEdit> that was undone.
*
* Event: mxEvent.REDO
*
* Fires afer a significant edit was redone in <redo>. The <code>edit</code>
* property contains the <mxUndoableEdit> that was redone.
*
* Event: mxEvent.ADD
*
* Fires after an undoable edit was added to the history. The <code>edit</code>
* property contains the <mxUndoableEdit> that was added.
*
* Constructor: mxUndoManager
*
* Constructs a new undo manager with the given history size. If no history
* size is given, then a default size of 100 steps is used.
*/
function mxUndoManager(size)
{
this.size = (size != null) ? size : 100;
this.clear();
};
/**
* Extends mxEventSource.
*/
mxUndoManager.prototype = new mxEventSource();
mxUndoManager.prototype.constructor = mxUndoManager;
/**
* Variable: size
*
* Maximum command history size. 0 means unlimited history. Default is
* 100.
*/
mxUndoManager.prototype.size = null;
/**
* Variable: history
*
* Array that contains the steps of the command history.
*/
mxUndoManager.prototype.history = null;
/**
* Variable: indexOfNextAdd
*
* Index of the element to be added next.
*/
mxUndoManager.prototype.indexOfNextAdd = 0;
/**
* Function: isEmpty
*
* Returns true if the history is empty.
*/
mxUndoManager.prototype.isEmpty = function()
{
return this.history.length == 0;
};
/**
* Function: clear
*
* Clears the command history.
*/
mxUndoManager.prototype.clear = function()
{
this.history = [];
this.indexOfNextAdd = 0;
this.fireEvent(new mxEventObject(mxEvent.CLEAR));
};
/**
* Function: canUndo
*
* Returns true if an undo is possible.
*/
mxUndoManager.prototype.canUndo = function()
{
return this.indexOfNextAdd > 0;
};
/**
* Function: undo
*
* Undoes the last change.
*/
mxUndoManager.prototype.undo = function()
{
while (this.indexOfNextAdd > 0)
{
var edit = this.history[--this.indexOfNextAdd];
edit.undo();
if (edit.isSignificant())
{
this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
break;
}
}
};
/**
* Function: canRedo
*
* Returns true if a redo is possible.
*/
mxUndoManager.prototype.canRedo = function()
{
return this.indexOfNextAdd < this.history.length;
};
/**
* Function: redo
*
* Redoes the last change.
*/
mxUndoManager.prototype.redo = function()
{
var n = this.history.length;
while (this.indexOfNextAdd < n)
{
var edit = this.history[this.indexOfNextAdd++];
edit.redo();
if (edit.isSignificant())
{
this.fireEvent(new mxEventObject(mxEvent.REDO, 'edit', edit));
break;
}
}
};
/**
* Function: undoableEditHappened
*
* Method to be called to add new undoable edits to the <history>.
*/
mxUndoManager.prototype.undoableEditHappened = function(undoableEdit)
{
this.trim();
if (this.size > 0 &&
this.size == this.history.length)
{
this.history.shift();
}
this.history.push(undoableEdit);
this.indexOfNextAdd = this.history.length;
this.fireEvent(new mxEventObject(mxEvent.ADD, 'edit', undoableEdit));
};
/**
* Function: trim
*
* Removes all pending steps after <indexOfNextAdd> from the history,
* invoking die on each edit. This is called from <undoableEditHappened>.
*/
mxUndoManager.prototype.trim = function()
{
if (this.history.length > this.indexOfNextAdd)
{
var edits = this.history.splice(this.indexOfNextAdd,
this.history.length - this.indexOfNextAdd);
for (var i = 0; i < edits.length; i++)
{
edits[i].die();
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
*
* Class: mxUrlConverter
*
* Converts relative and absolute URLs to absolute URLs with protocol and domain.
*/
var mxUrlConverter = function()
{
// Empty constructor
};
/**
* Variable: enabled
*
* Specifies if the converter is enabled. Default is true.
*/
mxUrlConverter.prototype.enabled = true;
/**
* Variable: baseUrl
*
* Specifies the base URL to be used as a prefix for relative URLs.
*/
mxUrlConverter.prototype.baseUrl = null;
/**
* Variable: baseDomain
*
* Specifies the base domain to be used as a prefix for absolute URLs.
*/
mxUrlConverter.prototype.baseDomain = null;
/**
* Function: updateBaseUrl
*
* Private helper function to update the base URL.
*/
mxUrlConverter.prototype.updateBaseUrl = function()
{
this.baseDomain = location.protocol + '//' + location.host;
this.baseUrl = this.baseDomain + location.pathname;
var tmp = this.baseUrl.lastIndexOf('/');
// Strips filename etc
if (tmp > 0)
{
this.baseUrl = this.baseUrl.substring(0, tmp + 1);
}
};
/**
* Function: isEnabled
*
* Returns <enabled>.
*/
mxUrlConverter.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Sets <enabled>.
*/
mxUrlConverter.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: getBaseUrl
*
* Returns <baseUrl>.
*/
mxUrlConverter.prototype.getBaseUrl = function()
{
return this.baseUrl;
};
/**
* Function: setBaseUrl
*
* Sets <baseUrl>.
*/
mxUrlConverter.prototype.setBaseUrl = function(value)
{
this.baseUrl = value;
};
/**
* Function: getBaseDomain
*
* Returns <baseDomain>.
*/
mxUrlConverter.prototype.getBaseDomain = function()
{
return this.baseDomain;
},
/**
* Function: setBaseDomain
*
* Sets <baseDomain>.
*/
mxUrlConverter.prototype.setBaseDomain = function(value)
{
this.baseDomain = value;
},
/**
* Function: isRelativeUrl
*
* Returns true if the given URL is relative.
*/
mxUrlConverter.prototype.isRelativeUrl = function(url)
{
return url.substring(0, 2) != '//' && url.substring(0, 7) != 'http://' &&
url.substring(0, 8) != 'https://' && url.substring(0, 10) != 'data:image' &&
url.substring(0, 7) != 'file://';
};
/**
* Function: convert
*
* Converts the given URL to an absolute URL with protol and domain.
* Relative URLs are first converted to absolute URLs.
*/
mxUrlConverter.prototype.convert = function(url)
{
if (this.isEnabled() && this.isRelativeUrl(url))
{
if (this.getBaseUrl() == null)
{
this.updateBaseUrl();
}
if (url.charAt(0) == '/')
{
url = this.getBaseDomain() + url;
}
else
{
url = this.getBaseUrl() + url;
}
}
return url;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPanningManager
*
* Implements a handler for panning.
*/
function mxPanningManager(graph)
{
this.thread = null;
this.active = false;
this.tdx = 0;
this.tdy = 0;
this.t0x = 0;
this.t0y = 0;
this.dx = 0;
this.dy = 0;
this.scrollbars = false;
this.scrollLeft = 0;
this.scrollTop = 0;
this.mouseListener =
{
mouseDown: function(sender, me) { },
mouseMove: function(sender, me) { },
mouseUp: mxUtils.bind(this, function(sender, me)
{
if (this.active)
{
this.stop();
}
})
};
graph.addMouseListener(this.mouseListener);
this.mouseUpListener = mxUtils.bind(this, function()
{
if (this.active)
{
this.stop();
}
});
// Stops scrolling on every mouseup anywhere in the document
mxEvent.addListener(document, 'mouseup', this.mouseUpListener);
var createThread = mxUtils.bind(this, function()
{
this.scrollbars = mxUtils.hasScrollbars(graph.container);
this.scrollLeft = graph.container.scrollLeft;
this.scrollTop = graph.container.scrollTop;
return window.setInterval(mxUtils.bind(this, function()
{
this.tdx -= this.dx;
this.tdy -= this.dy;
if (this.scrollbars)
{
var left = -graph.container.scrollLeft - Math.ceil(this.dx);
var top = -graph.container.scrollTop - Math.ceil(this.dy);
graph.panGraph(left, top);
graph.panDx = this.scrollLeft - graph.container.scrollLeft;
graph.panDy = this.scrollTop - graph.container.scrollTop;
graph.fireEvent(new mxEventObject(mxEvent.PAN));
// TODO: Implement graph.autoExtend
}
else
{
graph.panGraph(this.getDx(), this.getDy());
}
}), this.delay);
});
this.isActive = function()
{
return active;
};
this.getDx = function()
{
return Math.round(this.tdx);
};
this.getDy = function()
{
return Math.round(this.tdy);
};
this.start = function()
{
this.t0x = graph.view.translate.x;
this.t0y = graph.view.translate.y;
this.active = true;
};
this.panTo = function(x, y, w, h)
{
if (!this.active)
{
this.start();
}
this.scrollLeft = graph.container.scrollLeft;
this.scrollTop = graph.container.scrollTop;
w = (w != null) ? w : 0;
h = (h != null) ? h : 0;
var c = graph.container;
this.dx = x + w - c.scrollLeft - c.clientWidth;
if (this.dx < 0 && Math.abs(this.dx) < this.border)
{
this.dx = this.border + this.dx;
}
else if (this.handleMouseOut)
{
this.dx = Math.max(this.dx, 0);
}
else
{
this.dx = 0;
}
if (this.dx == 0)
{
this.dx = x - c.scrollLeft;
if (this.dx > 0 && this.dx < this.border)
{
this.dx = this.dx - this.border;
}
else if (this.handleMouseOut)
{
this.dx = Math.min(0, this.dx);
}
else
{
this.dx = 0;
}
}
this.dy = y + h - c.scrollTop - c.clientHeight;
if (this.dy < 0 && Math.abs(this.dy) < this.border)
{
this.dy = this.border + this.dy;
}
else if (this.handleMouseOut)
{
this.dy = Math.max(this.dy, 0);
}
else
{
this.dy = 0;
}
if (this.dy == 0)
{
this.dy = y - c.scrollTop;
if (this.dy > 0 && this.dy < this.border)
{
this.dy = this.dy - this.border;
}
else if (this.handleMouseOut)
{
this.dy = Math.min(0, this.dy);
}
else
{
this.dy = 0;
}
}
if (this.dx != 0 || this.dy != 0)
{
this.dx *= this.damper;
this.dy *= this.damper;
if (this.thread == null)
{
this.thread = createThread();
}
}
else if (this.thread != null)
{
window.clearInterval(this.thread);
this.thread = null;
}
};
this.stop = function()
{
if (this.active)
{
this.active = false;
if (this.thread != null)
{
window.clearInterval(this.thread);
this.thread = null;
}
this.tdx = 0;
this.tdy = 0;
if (!this.scrollbars)
{
var px = graph.panDx;
var py = graph.panDy;
if (px != 0 || py != 0)
{
graph.panGraph(0, 0);
graph.view.setTranslate(this.t0x + px / graph.view.scale, this.t0y + py / graph.view.scale);
}
}
else
{
graph.panDx = 0;
graph.panDy = 0;
graph.fireEvent(new mxEventObject(mxEvent.PAN));
}
}
};
this.destroy = function()
{
graph.removeMouseListener(this.mouseListener);
mxEvent.removeListener(document, 'mouseup', this.mouseUpListener);
};
};
/**
* Variable: damper
*
* Damper value for the panning. Default is 1/6.
*/
mxPanningManager.prototype.damper = 1/6;
/**
* Variable: delay
*
* Delay in milliseconds for the panning. Default is 10.
*/
mxPanningManager.prototype.delay = 10;
/**
* Variable: handleMouseOut
*
* Specifies if mouse events outside of the component should be handled. Default is true.
*/
mxPanningManager.prototype.handleMouseOut = true;
/**
* Variable: border
*
* Border to handle automatic panning inside the component. Default is 0 (disabled).
*/
mxPanningManager.prototype.border = 0;
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPopupMenu
*
* Basic popup menu. To add a vertical scrollbar to a given submenu, the
* following code can be used.
*
* (code)
* var mxPopupMenuShowMenu = mxPopupMenu.prototype.showMenu;
* mxPopupMenu.prototype.showMenu = function()
* {
* mxPopupMenuShowMenu.apply(this, arguments);
*
* this.div.style.overflowY = 'auto';
* this.div.style.overflowX = 'hidden';
* this.div.style.maxHeight = '160px';
* };
* (end)
*
* Constructor: mxPopupMenu
*
* Constructs a popupmenu.
*
* Event: mxEvent.SHOW
*
* Fires after the menu has been shown in <popup>.
*/
function mxPopupMenu(factoryMethod)
{
this.factoryMethod = factoryMethod;
if (factoryMethod != null)
{
this.init();
}
};
/**
* Extends mxEventSource.
*/
mxPopupMenu.prototype = new mxEventSource();
mxPopupMenu.prototype.constructor = mxPopupMenu;
/**
* Variable: submenuImage
*
* URL of the image to be used for the submenu icon.
*/
mxPopupMenu.prototype.submenuImage = mxClient.imageBasePath + '/submenu.gif';
/**
* Variable: zIndex
*
* Specifies the zIndex for the popupmenu and its shadow. Default is 10006.
*/
mxPopupMenu.prototype.zIndex = 10006;
/**
* Variable: factoryMethod
*
* Function that is used to create the popup menu. The function takes the
* current panning handler, the <mxCell> under the mouse and the mouse
* event that triggered the call as arguments.
*/
mxPopupMenu.prototype.factoryMethod = null;
/**
* Variable: useLeftButtonForPopup
*
* Specifies if popupmenus should be activated by clicking the left mouse
* button. Default is false.
*/
mxPopupMenu.prototype.useLeftButtonForPopup = false;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxPopupMenu.prototype.enabled = true;
/**
* Variable: itemCount
*
* Contains the number of times <addItem> has been called for a new menu.
*/
mxPopupMenu.prototype.itemCount = 0;
/**
* Variable: autoExpand
*
* Specifies if submenus should be expanded on mouseover. Default is false.
*/
mxPopupMenu.prototype.autoExpand = false;
/**
* Variable: smartSeparators
*
* Specifies if separators should only be added if a menu item follows them.
* Default is false.
*/
mxPopupMenu.prototype.smartSeparators = false;
/**
* Variable: labels
*
* Specifies if any labels should be visible. Default is true.
*/
mxPopupMenu.prototype.labels = true;
/**
* Function: init
*
* Initializes the shapes required for this vertex handler.
*/
mxPopupMenu.prototype.init = function()
{
// Adds the inner table
this.table = document.createElement('table');
this.table.className = 'mxPopupMenu';
this.tbody = document.createElement('tbody');
this.table.appendChild(this.tbody);
// Adds the outer div
this.div = document.createElement('div');
this.div.className = 'mxPopupMenu';
this.div.style.display = 'inline';
this.div.style.zIndex = this.zIndex;
this.div.appendChild(this.table);
// Disables the context menu on the outer div
mxEvent.disableContextMenu(this.div);
};
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxPopupMenu.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*/
mxPopupMenu.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isPopupTrigger
*
* Returns true if the given event is a popupmenu trigger for the optional
* given cell.
*
* Parameters:
*
* me - <mxMouseEvent> that represents the mouse event.
*/
mxPopupMenu.prototype.isPopupTrigger = function(me)
{
return me.isPopupTrigger() || (this.useLeftButtonForPopup && mxEvent.isLeftMouseButton(me.getEvent()));
};
/**
* Function: addItem
*
* Adds the given item to the given parent item. If no parent item is specified
* then the item is added to the top-level menu. The return value may be used
* as the parent argument, ie. as a submenu item. The return value is the table
* row that represents the item.
*
* Paramters:
*
* title - String that represents the title of the menu item.
* image - Optional URL for the image icon.
* funct - Function associated that takes a mouseup or touchend event.
* parent - Optional item returned by <addItem>.
* iconCls - Optional string that represents the CSS class for the image icon.
* IconsCls is ignored if image is given.
* enabled - Optional boolean indicating if the item is enabled. Default is true.
* active - Optional boolean indicating if the menu should implement any event handling.
* Default is true.
*/
mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled, active)
{
parent = parent || this;
this.itemCount++;
// Smart separators only added if element contains items
if (parent.willAddSeparator)
{
if (parent.containsItems)
{
this.addSeparator(parent, true);
}
parent.willAddSeparator = false;
}
parent.containsItems = true;
var tr = document.createElement('tr');
tr.className = 'mxPopupMenuItem';
var col1 = document.createElement('td');
col1.className = 'mxPopupMenuIcon';
// Adds the given image into the first column
if (image != null)
{
var img = document.createElement('img');
img.src = image;
col1.appendChild(img);
}
else if (iconCls != null)
{
var div = document.createElement('div');
div.className = iconCls;
col1.appendChild(div);
}
tr.appendChild(col1);
if (this.labels)
{
var col2 = document.createElement('td');
col2.className = 'mxPopupMenuItem' +
((enabled != null && !enabled) ? ' mxDisabled' : '');
mxUtils.write(col2, title);
col2.align = 'left';
tr.appendChild(col2);
var col3 = document.createElement('td');
col3.className = 'mxPopupMenuItem' +
((enabled != null && !enabled) ? ' mxDisabled' : '');
col3.style.paddingRight = '6px';
col3.style.textAlign = 'right';
tr.appendChild(col3);
if (parent.div == null)
{
this.createSubmenu(parent);
}
}
parent.tbody.appendChild(tr);
if (active != false && enabled != false)
{
var currentSelection = null;
mxEvent.addGestureListeners(tr,
mxUtils.bind(this, function(evt)
{
this.eventReceiver = tr;
if (parent.activeRow != tr && parent.activeRow != parent)
{
if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
{
this.hideSubmenu(parent);
}
if (tr.div != null)
{
this.showSubmenu(parent, tr);
parent.activeRow = tr;
}
}
// Workaround for lost current selection in page because of focus in IE
if (document.selection != null && (mxClient.IS_QUIRKS || document.documentMode == 8))
{
currentSelection = document.selection.createRange();
}
mxEvent.consume(evt);
}),
mxUtils.bind(this, function(evt)
{
if (parent.activeRow != tr && parent.activeRow != parent)
{
if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
{
this.hideSubmenu(parent);
}
if (this.autoExpand && tr.div != null)
{
this.showSubmenu(parent, tr);
parent.activeRow = tr;
}
}
// Sets hover style because TR in IE doesn't have hover
tr.className = 'mxPopupMenuItemHover';
}),
mxUtils.bind(this, function(evt)
{
// EventReceiver avoids clicks on a submenu item
// which has just been shown in the mousedown
if (this.eventReceiver == tr)
{
if (parent.activeRow != tr)
{
this.hideMenu();
}
// Workaround for lost current selection in page because of focus in IE
if (currentSelection != null)
{
// Workaround for "unspecified error" in IE8 standards
try
{
currentSelection.select();
}
catch (e)
{
// ignore
}
currentSelection = null;
}
if (funct != null)
{
funct(evt);
}
}
this.eventReceiver = null;
mxEvent.consume(evt);
})
);
// Resets hover style because TR in IE doesn't have hover
mxEvent.addListener(tr, 'mouseout',
mxUtils.bind(this, function(evt)
{
tr.className = 'mxPopupMenuItem';
})
);
}
return tr;
};
/**
* Adds a checkmark to the given menuitem.
*/
mxPopupMenu.prototype.addCheckmark = function(item, img)
{
var td = item.firstChild.nextSibling;
td.style.backgroundImage = 'url(\'' + img + '\')';
td.style.backgroundRepeat = 'no-repeat';
td.style.backgroundPosition = '2px 50%';
};
/**
* Function: createSubmenu
*
* Creates the nodes required to add submenu items inside the given parent
* item. This is called in <addItem> if a parent item is used for the first
* time. This adds various DOM nodes and a <submenuImage> to the parent.
*
* Parameters:
*
* parent - An item returned by <addItem>.
*/
mxPopupMenu.prototype.createSubmenu = function(parent)
{
parent.table = document.createElement('table');
parent.table.className = 'mxPopupMenu';
parent.tbody = document.createElement('tbody');
parent.table.appendChild(parent.tbody);
parent.div = document.createElement('div');
parent.div.className = 'mxPopupMenu';
parent.div.style.position = 'absolute';
parent.div.style.display = 'inline';
parent.div.style.zIndex = this.zIndex;
parent.div.appendChild(parent.table);
var img = document.createElement('img');
img.setAttribute('src', this.submenuImage);
// Last column of the submenu item in the parent menu
td = parent.firstChild.nextSibling.nextSibling;
td.appendChild(img);
};
/**
* Function: showSubmenu
*
* Shows the submenu inside the given parent row.
*/
mxPopupMenu.prototype.showSubmenu = function(parent, row)
{
if (row.div != null)
{
row.div.style.left = (parent.div.offsetLeft +
row.offsetLeft+row.offsetWidth - 1) + 'px';
row.div.style.top = (parent.div.offsetTop+row.offsetTop) + 'px';
document.body.appendChild(row.div);
// Moves the submenu to the left side if there is no space
var left = parseInt(row.div.offsetLeft);
var width = parseInt(row.div.offsetWidth);
var offset = mxUtils.getDocumentScrollOrigin(document);
var b = document.body;
var d = document.documentElement;
var right = offset.x + (b.clientWidth || d.clientWidth);
if (left + width > right)
{
row.div.style.left = Math.max(0, (parent.div.offsetLeft - width + ((mxClient.IS_IE) ? 6 : -6))) + 'px';
}
mxUtils.fit(row.div);
}
};
/**
* Function: addSeparator
*
* Adds a horizontal separator in the given parent item or the top-level menu
* if no parent is specified.
*
* Parameters:
*
* parent - Optional item returned by <addItem>.
* force - Optional boolean to ignore <smartSeparators>. Default is false.
*/
mxPopupMenu.prototype.addSeparator = function(parent, force)
{
parent = parent || this;
if (this.smartSeparators && !force)
{
parent.willAddSeparator = true;
}
else if (parent.tbody != null)
{
parent.willAddSeparator = false;
var tr = document.createElement('tr');
var col1 = document.createElement('td');
col1.className = 'mxPopupMenuIcon';
col1.style.padding = '0 0 0 0px';
tr.appendChild(col1);
var col2 = document.createElement('td');
col2.style.padding = '0 0 0 0px';
col2.setAttribute('colSpan', '2');
var hr = document.createElement('hr');
hr.setAttribute('size', '1');
col2.appendChild(hr);
tr.appendChild(col2);
parent.tbody.appendChild(tr);
}
};
/**
* Function: popup
*
* Shows the popup menu for the given event and cell.
*
* Example:
*
* (code)
* graph.panningHandler.popup = function(x, y, cell, evt)
* {
* mxUtils.alert('Hello, World!');
* }
* (end)
*/
mxPopupMenu.prototype.popup = function(x, y, cell, evt)
{
if (this.div != null && this.tbody != null && this.factoryMethod != null)
{
this.div.style.left = x + 'px';
this.div.style.top = y + 'px';
// Removes all child nodes from the existing menu
while (this.tbody.firstChild != null)
{
mxEvent.release(this.tbody.firstChild);
this.tbody.removeChild(this.tbody.firstChild);
}
this.itemCount = 0;
this.factoryMethod(this, cell, evt);
if (this.itemCount > 0)
{
this.showMenu();
this.fireEvent(new mxEventObject(mxEvent.SHOW));
}
}
};
/**
* Function: isMenuShowing
*
* Returns true if the menu is showing.
*/
mxPopupMenu.prototype.isMenuShowing = function()
{
return this.div != null && this.div.parentNode == document.body;
};
/**
* Function: showMenu
*
* Shows the menu.
*/
mxPopupMenu.prototype.showMenu = function()
{
// Disables filter-based shadow in IE9 standards mode
if (document.documentMode >= 9)
{
this.div.style.filter = 'none';
}
// Fits the div inside the viewport
document.body.appendChild(this.div);
mxUtils.fit(this.div);
};
/**
* Function: hideMenu
*
* Removes the menu and all submenus.
*/
mxPopupMenu.prototype.hideMenu = function()
{
if (this.div != null)
{
if (this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.hideSubmenu(this);
this.containsItems = false;
this.fireEvent(new mxEventObject(mxEvent.HIDE));
}
};
/**
* Function: hideSubmenu
*
* Removes all submenus inside the given parent.
*
* Parameters:
*
* parent - An item returned by <addItem>.
*/
mxPopupMenu.prototype.hideSubmenu = function(parent)
{
if (parent.activeRow != null)
{
this.hideSubmenu(parent.activeRow);
if (parent.activeRow.div.parentNode != null)
{
parent.activeRow.div.parentNode.removeChild(parent.activeRow.div);
}
parent.activeRow = null;
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPopupMenu.prototype.destroy = function()
{
if (this.div != null)
{
mxEvent.release(this.div);
if (this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.div = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxAutoSaveManager
*
* Manager for automatically saving diagrams. The <save> hook must be
* implemented.
*
* Example:
*
* (code)
* var mgr = new mxAutoSaveManager(editor.graph);
* mgr.save = function()
* {
* mxLog.show();
* mxLog.debug('save');
* };
* (end)
*
* Constructor: mxAutoSaveManager
*
* Constructs a new automatic layout for the given graph.
*
* Arguments:
*
* graph - Reference to the enclosing graph.
*/
function mxAutoSaveManager(graph)
{
// Notifies the manager of a change
this.changeHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.graphModelChanged(evt.getProperty('edit').changes);
}
});
this.setGraph(graph);
};
/**
* Extends mxEventSource.
*/
mxAutoSaveManager.prototype = new mxEventSource();
mxAutoSaveManager.prototype.constructor = mxAutoSaveManager;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxAutoSaveManager.prototype.graph = null;
/**
* Variable: autoSaveDelay
*
* Minimum amount of seconds between two consecutive autosaves. Eg. a
* value of 1 (s) means the graph is not stored more than once per second.
* Default is 10.
*/
mxAutoSaveManager.prototype.autoSaveDelay = 10;
/**
* Variable: autoSaveThrottle
*
* Minimum amount of seconds between two consecutive autosaves triggered by
* more than <autoSaveThreshhold> changes within a timespan of less than
* <autoSaveDelay> seconds. Eg. a value of 1 (s) means the graph is not
* stored more than once per second even if there are more than
* <autoSaveThreshold> changes within that timespan. Default is 2.
*/
mxAutoSaveManager.prototype.autoSaveThrottle = 2;
/**
* Variable: autoSaveThreshold
*
* Minimum amount of ignored changes before an autosave. Eg. a value of 2
* means after 2 change of the graph model the autosave will trigger if the
* condition below is true. Default is 5.
*/
mxAutoSaveManager.prototype.autoSaveThreshold = 5;
/**
* Variable: ignoredChanges
*
* Counter for ignored changes in autosave.
*/
mxAutoSaveManager.prototype.ignoredChanges = 0;
/**
* Variable: lastSnapshot
*
* Used for autosaving. See <autosave>.
*/
mxAutoSaveManager.prototype.lastSnapshot = 0;
/**
* Variable: enabled
*
* Specifies if event handling is enabled. Default is true.
*/
mxAutoSaveManager.prototype.enabled = true;
/**
* Variable: changeHandler
*
* Holds the function that handles graph model changes.
*/
mxAutoSaveManager.prototype.changeHandler = null;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxAutoSaveManager.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxAutoSaveManager.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: setGraph
*
* Sets the graph that the layouts operate on.
*/
mxAutoSaveManager.prototype.setGraph = function(graph)
{
if (this.graph != null)
{
this.graph.getModel().removeListener(this.changeHandler);
}
this.graph = graph;
if (this.graph != null)
{
this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
}
};
/**
* Function: save
*
* Empty hook that is called if the graph should be saved.
*/
mxAutoSaveManager.prototype.save = function()
{
// empty
};
/**
* Function: graphModelChanged
*
* Invoked when the graph model has changed.
*/
mxAutoSaveManager.prototype.graphModelChanged = function(changes)
{
var now = new Date().getTime();
var dt = (now - this.lastSnapshot) / 1000;
if (dt > this.autoSaveDelay ||
(this.ignoredChanges >= this.autoSaveThreshold &&
dt > this.autoSaveThrottle))
{
this.save();
this.reset();
}
else
{
// Increments the number of ignored changes
this.ignoredChanges++;
}
};
/**
* Function: reset
*
* Resets all counters.
*/
mxAutoSaveManager.prototype.reset = function()
{
this.lastSnapshot = new Date().getTime();
this.ignoredChanges = 0;
};
/**
* Function: destroy
*
* Removes all handlers from the <graph> and deletes the reference to it.
*/
mxAutoSaveManager.prototype.destroy = function()
{
this.setGraph(null);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
*
* Class: mxAnimation
*
* Implements a basic animation in JavaScript.
*
* Constructor: mxAnimation
*
* Constructs an animation.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxAnimation(delay)
{
this.delay = (delay != null) ? delay : 20;
};
/**
* Extends mxEventSource.
*/
mxAnimation.prototype = new mxEventSource();
mxAnimation.prototype.constructor = mxAnimation;
/**
* Variable: delay
*
* Specifies the delay between the animation steps. Defaul is 30ms.
*/
mxAnimation.prototype.delay = null;
/**
* Variable: thread
*
* Reference to the thread while the animation is running.
*/
mxAnimation.prototype.thread = null;
/**
* Function: isRunning
*
* Returns true if the animation is running.
*/
mxAnimation.prototype.isRunning = function()
{
return this.thread != null;
};
/**
* Function: startAnimation
*
* Starts the animation by repeatedly invoking updateAnimation.
*/
mxAnimation.prototype.startAnimation = function()
{
if (this.thread == null)
{
this.thread = window.setInterval(mxUtils.bind(this, this.updateAnimation), this.delay);
}
};
/**
* Function: updateAnimation
*
* Hook for subclassers to implement the animation. Invoke stopAnimation
* when finished, startAnimation to resume. This is called whenever the
* timer fires and fires an mxEvent.EXECUTE event with no properties.
*/
mxAnimation.prototype.updateAnimation = function()
{
this.fireEvent(new mxEventObject(mxEvent.EXECUTE));
};
/**
* Function: stopAnimation
*
* Stops the animation by deleting the timer and fires an <mxEvent.DONE>.
*/
mxAnimation.prototype.stopAnimation = function()
{
if (this.thread != null)
{
window.clearInterval(this.thread);
this.thread = null;
this.fireEvent(new mxEventObject(mxEvent.DONE));
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
*
* Class: mxMorphing
*
* Implements animation for morphing cells. Here is an example of
* using this class for animating the result of a layout algorithm:
*
* (code)
* graph.getModel().beginUpdate();
* try
* {
* var circleLayout = new mxCircleLayout(graph);
* circleLayout.execute(graph.getDefaultParent());
* }
* finally
* {
* var morph = new mxMorphing(graph);
* morph.addListener(mxEvent.DONE, function()
* {
* graph.getModel().endUpdate();
* });
*
* morph.startAnimation();
* }
* (end)
*
* Constructor: mxMorphing
*
* Constructs an animation.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* steps - Optional number of steps in the morphing animation. Default is 6.
* ease - Optional easing constant for the animation. Default is 1.5.
* delay - Optional delay between the animation steps. Passed to <mxAnimation>.
*/
function mxMorphing(graph, steps, ease, delay)
{
mxAnimation.call(this, delay);
this.graph = graph;
this.steps = (steps != null) ? steps : 6;
this.ease = (ease != null) ? ease : 1.5;
};
/**
* Extends mxEventSource.
*/
mxMorphing.prototype = new mxAnimation();
mxMorphing.prototype.constructor = mxMorphing;
/**
* Variable: graph
*
* Specifies the delay between the animation steps. Defaul is 30ms.
*/
mxMorphing.prototype.graph = null;
/**
* Variable: steps
*
* Specifies the maximum number of steps for the morphing.
*/
mxMorphing.prototype.steps = null;
/**
* Variable: step
*
* Contains the current step.
*/
mxMorphing.prototype.step = 0;
/**
* Variable: ease
*
* Ease-off for movement towards the given vector. Larger values are
* slower and smoother. Default is 4.
*/
mxMorphing.prototype.ease = null;
/**
* Variable: cells
*
* Optional array of cells to be animated. If this is not specified
* then all cells are checked and animated if they have been moved
* in the current transaction.
*/
mxMorphing.prototype.cells = null;
/**
* Function: updateAnimation
*
* Animation step.
*/
mxMorphing.prototype.updateAnimation = function()
{
mxAnimation.prototype.updateAnimation.apply(this, arguments);
var move = new mxCellStatePreview(this.graph);
if (this.cells != null)
{
// Animates the given cells individually without recursion
for (var i = 0; i < this.cells.length; i++)
{
this.animateCell(this.cells[i], move, false);
}
}
else
{
// Animates all changed cells by using recursion to find
// the changed cells but not for the animation itself
this.animateCell(this.graph.getModel().getRoot(), move, true);
}
this.show(move);
if (move.isEmpty() || this.step++ >= this.steps)
{
this.stopAnimation();
}
};
/**
* Function: show
*
* Shows the changes in the given <mxCellStatePreview>.
*/
mxMorphing.prototype.show = function(move)
{
move.show();
};
/**
* Function: animateCell
*
* Animates the given cell state using <mxCellStatePreview.moveState>.
*/
mxMorphing.prototype.animateCell = function(cell, move, recurse)
{
var state = this.graph.getView().getState(cell);
var delta = null;
if (state != null)
{
// Moves the animated state from where it will be after the model
// change by subtracting the given delta vector from that location
delta = this.getDelta(state);
if (this.graph.getModel().isVertex(cell) && (delta.x != 0 || delta.y != 0))
{
var translate = this.graph.view.getTranslate();
var scale = this.graph.view.getScale();
delta.x += translate.x * scale;
delta.y += translate.y * scale;
move.moveState(state, -delta.x / this.ease, -delta.y / this.ease);
}
}
if (recurse && !this.stopRecursion(state, delta))
{
var childCount = this.graph.getModel().getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.animateCell(this.graph.getModel().getChildAt(cell, i), move, recurse);
}
}
};
/**
* Function: stopRecursion
*
* Returns true if the animation should not recursively find more
* deltas for children if the given parent state has been animated.
*/
mxMorphing.prototype.stopRecursion = function(state, delta)
{
return delta != null && (delta.x != 0 || delta.y != 0);
};
/**
* Function: getDelta
*
* Returns the vector between the current rendered state and the future
* location of the state after the display will be updated.
*/
mxMorphing.prototype.getDelta = function(state)
{
var origin = this.getOriginForCell(state.cell);
var translate = this.graph.getView().getTranslate();
var scale = this.graph.getView().getScale();
var x = state.x / scale - translate.x;
var y = state.y / scale - translate.y;
return new mxPoint((origin.x - x) * scale, (origin.y - y) * scale);
};
/**
* Function: getOriginForCell
*
* Returns the top, left corner of the given cell. TODO: Improve performance
* by using caching inside this method as the result per cell never changes
* during the lifecycle of this object.
*/
mxMorphing.prototype.getOriginForCell = function(cell)
{
var result = null;
if (cell != null)
{
var parent = this.graph.getModel().getParent(cell);
var geo = this.graph.getCellGeometry(cell);
result = this.getOriginForCell(parent);
// TODO: Handle offsets
if (geo != null)
{
if (geo.relative)
{
var pgeo = this.graph.getCellGeometry(parent);
if (pgeo != null)
{
result.x += geo.x * pgeo.width;
result.y += geo.y * pgeo.height;
}
}
else
{
result.x += geo.x;
result.y += geo.y;
}
}
}
if (result == null)
{
var t = this.graph.view.getTranslate();
result = new mxPoint(-t.x, -t.y);
}
return result;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxImageBundle
*
* Maps from keys to base64 encoded images or file locations. All values must
* be URLs or use the format data:image/format followed by a comma and the base64
* encoded image data, eg. "data:image/gif,XYZ", where XYZ is the base64 encoded
* image data.
*
* To add a new image bundle to an existing graph, the following code is used:
*
* (code)
* var bundle = new mxImageBundle(alt);
* bundle.putImage('myImage', 'data:image/gif,R0lGODlhEAAQAMIGAAAAAICAAICAgP' +
* '//AOzp2O3r2////////yH+FUNyZWF0ZWQgd2l0aCBUaGUgR0lNUAAh+QQBCgAHACwAAAAA' +
* 'EAAQAAADTXi63AowynnAMDfjPUDlnAAJhmeBFxAEloliKltWmiYCQvfVr6lBPB1ggxN1hi' +
* 'laSSASFQpIV5HJBDyHpqK2ejVRm2AAgZCdmCGO9CIBADs=', fallback);
* bundle.putImage('mySvgImage', 'data:image/svg+xml,' + encodeURIComponent(
* '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">' +
* '<linearGradient id="gradient"><stop offset="10%" stop-color="#F00"/>' +
* '<stop offset="90%" stop-color="#fcc"/></linearGradient>' +
* '<rect fill="url(#gradient)" width="100%" height="100%"/></svg>'), fallback);
* graph.addImageBundle(bundle);
* (end);
*
* Alt is an optional boolean (default is false) that specifies if the value
* or the fallback should be returned in <getImage>.
*
* The image can then be referenced in any cell style using image=myImage.
* If you are using mxOutline, you should use the same image bundles in the
* graph that renders the outline.
*
* The keys for images are resolved in <mxGraph.postProcessCellStyle> and
* turned into a data URI if the returned value has a short data URI format
* as specified above.
*
* A typical value for the fallback is a MTHML link as defined in RFC 2557.
* Note that this format requires a file to be dynamically created on the
* server-side, or the page that contains the graph to be modified to contain
* the resources, this can be done by adding a comment that contains the
* resource in the HEAD section of the page after the title tag.
*
* This type of fallback mechanism should be used in IE6 and IE7. IE8 does
* support data URIs, but the maximum size is limited to 32 KB, which means
* all data URIs should be limited to 32 KB.
*/
function mxImageBundle(alt)
{
this.images = [];
this.alt = (alt != null) ? alt : false;
};
/**
* Variable: images
*
* Maps from keys to images.
*/
mxImageBundle.prototype.images = null;
/**
* Variable: alt
*
* Specifies if the fallback representation should be returned.
*/
mxImageBundle.prototype.images = null;
/**
* Function: putImage
*
* Adds the specified entry to the map. The entry is an object with a value and
* fallback property as specified in the arguments.
*/
mxImageBundle.prototype.putImage = function(key, value, fallback)
{
this.images[key] = {value: value, fallback: fallback};
};
/**
* Function: getImage
*
* Returns the value for the given key. This returns the value
* or fallback, depending on <alt>. The fallback is returned if
* <alt> is true, the value is returned otherwise.
*/
mxImageBundle.prototype.getImage = function(key)
{
var result = null;
if (key != null)
{
var img = this.images[key];
if (img != null)
{
result = (this.alt) ? img.fallback : img.value;
}
}
return result;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxImageExport
*
* Creates a new image export instance to be used with an export canvas. Here
* is an example that uses this class to create an image via a backend using
* <mxXmlExportCanvas>.
*
* (code)
* var xmlDoc = mxUtils.createXmlDocument();
* var root = xmlDoc.createElement('output');
* xmlDoc.appendChild(root);
*
* var xmlCanvas = new mxXmlCanvas2D(root);
* var imgExport = new mxImageExport();
* imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas);
*
* var bounds = graph.getGraphBounds();
* var w = Math.ceil(bounds.x + bounds.width);
* var h = Math.ceil(bounds.y + bounds.height);
*
* var xml = mxUtils.getXml(root);
* new mxXmlRequest('export', 'format=png&w=' + w +
* '&h=' + h + '&bg=#F9F7ED&xml=' + encodeURIComponent(xml))
* .simulate(document, '_blank');
* (end)
*
* Constructor: mxImageExport
*
* Constructs a new image export.
*/
function mxImageExport() { };
/**
* Variable: includeOverlays
*
* Specifies if overlays should be included in the export. Default is false.
*/
mxImageExport.prototype.includeOverlays = false;
/**
* Function: drawState
*
* Draws the given state and all its descendants to the given canvas.
*/
mxImageExport.prototype.drawState = function(state, canvas)
{
if (state != null)
{
this.visitStatesRecursive(state, canvas, mxUtils.bind(this, function()
{
this.drawCellState.apply(this, arguments);
}));
// Paints the overlays
if (this.includeOverlays)
{
this.visitStatesRecursive(state, canvas, mxUtils.bind(this, function()
{
this.drawOverlays.apply(this, arguments);
}));
}
}
};
/**
* Function: drawState
*
* Draws the given state and all its descendants to the given canvas.
*/
mxImageExport.prototype.visitStatesRecursive = function(state, canvas, visitor)
{
if (state != null)
{
visitor(state, canvas);
var graph = state.view.graph;
var childCount = graph.model.getChildCount(state.cell);
for (var i = 0; i < childCount; i++)
{
var childState = graph.view.getState(graph.model.getChildAt(state.cell, i));
this.visitStatesRecursive(childState, canvas, visitor);
}
}
};
/**
* Function: getLinkForCellState
*
* Returns the link for the given cell state and canvas. This returns null.
*/
mxImageExport.prototype.getLinkForCellState = function(state, canvas)
{
return null;
};
/**
* Function: drawCellState
*
* Draws the given state to the given canvas.
*/
mxImageExport.prototype.drawCellState = function(state, canvas)
{
// Experimental feature
var link = this.getLinkForCellState(state, canvas);
if (link != null)
{
canvas.setLink(link);
}
// Paints the shape and text
this.drawShape(state, canvas);
this.drawText(state, canvas);
if (link != null)
{
canvas.setLink(null);
}
};
/**
* Function: drawShape
*
* Draws the shape of the given state.
*/
mxImageExport.prototype.drawShape = function(state, canvas)
{
if (state.shape instanceof mxShape && state.shape.checkBounds())
{
canvas.save();
state.shape.paint(canvas);
canvas.restore();
}
};
/**
* Function: drawText
*
* Draws the text of the given state.
*/
mxImageExport.prototype.drawText = function(state, canvas)
{
if (state.text != null && state.text.checkBounds())
{
canvas.save();
state.text.paint(canvas);
canvas.restore();
}
};
/**
* Function: drawOverlays
*
* Draws the overlays for the given state. This is called if <includeOverlays>
* is true.
*/
mxImageExport.prototype.drawOverlays = function(state, canvas)
{
if (state.overlays != null)
{
state.overlays.visit(function(id, shape)
{
if (shape instanceof mxShape)
{
shape.paint(canvas);
}
});
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxAbstractCanvas2D
*
* Base class for all canvases. A description of the public API is available in <mxXmlCanvas2D>.
* All color values of <mxConstants.NONE> will be converted to null in the state.
*
* Constructor: mxAbstractCanvas2D
*
* Constructs a new abstract canvas.
*/
function mxAbstractCanvas2D()
{
/**
* Variable: converter
*
* Holds the <mxUrlConverter> to convert image URLs.
*/
this.converter = this.createUrlConverter();
this.reset();
};
/**
* Variable: state
*
* Holds the current state.
*/
mxAbstractCanvas2D.prototype.state = null;
/**
* Variable: states
*
* Stack of states.
*/
mxAbstractCanvas2D.prototype.states = null;
/**
* Variable: path
*
* Holds the current path as an array.
*/
mxAbstractCanvas2D.prototype.path = null;
/**
* Variable: rotateHtml
*
* Switch for rotation of HTML. Default is false.
*/
mxAbstractCanvas2D.prototype.rotateHtml = true;
/**
* Variable: lastX
*
* Holds the last x coordinate.
*/
mxAbstractCanvas2D.prototype.lastX = 0;
/**
* Variable: lastY
*
* Holds the last y coordinate.
*/
mxAbstractCanvas2D.prototype.lastY = 0;
/**
* Variable: moveOp
*
* Contains the string used for moving in paths. Default is 'M'.
*/
mxAbstractCanvas2D.prototype.moveOp = 'M';
/**
* Variable: lineOp
*
* Contains the string used for moving in paths. Default is 'L'.
*/
mxAbstractCanvas2D.prototype.lineOp = 'L';
/**
* Variable: quadOp
*
* Contains the string used for quadratic paths. Default is 'Q'.
*/
mxAbstractCanvas2D.prototype.quadOp = 'Q';
/**
* Variable: curveOp
*
* Contains the string used for bezier curves. Default is 'C'.
*/
mxAbstractCanvas2D.prototype.curveOp = 'C';
/**
* Variable: closeOp
*
* Holds the operator for closing curves. Default is 'Z'.
*/
mxAbstractCanvas2D.prototype.closeOp = 'Z';
/**
* Variable: pointerEvents
*
* Boolean value that specifies if events should be handled. Default is false.
*/
mxAbstractCanvas2D.prototype.pointerEvents = false;
/**
* Function: createUrlConverter
*
* Create a new <mxUrlConverter> and returns it.
*/
mxAbstractCanvas2D.prototype.createUrlConverter = function()
{
return new mxUrlConverter();
};
/**
* Function: reset
*
* Resets the state of this canvas.
*/
mxAbstractCanvas2D.prototype.reset = function()
{
this.state = this.createState();
this.states = [];
};
/**
* Function: createState
*
* Creates the state of the this canvas.
*/
mxAbstractCanvas2D.prototype.createState = function()
{
return {
dx: 0,
dy: 0,
scale: 1,
alpha: 1,
fillAlpha: 1,
strokeAlpha: 1,
fillColor: null,
gradientFillAlpha: 1,
gradientColor: null,
gradientAlpha: 1,
gradientDirection: null,
strokeColor: null,
strokeWidth: 1,
dashed: false,
dashPattern: '3 3',
fixDash: false,
lineCap: 'flat',
lineJoin: 'miter',
miterLimit: 10,
fontColor: '#000000',
fontBackgroundColor: null,
fontBorderColor: null,
fontSize: mxConstants.DEFAULT_FONTSIZE,
fontFamily: mxConstants.DEFAULT_FONTFAMILY,
fontStyle: 0,
shadow: false,
shadowColor: mxConstants.SHADOWCOLOR,
shadowAlpha: mxConstants.SHADOW_OPACITY,
shadowDx: mxConstants.SHADOW_OFFSET_X,
shadowDy: mxConstants.SHADOW_OFFSET_Y,
rotation: 0,
rotationCx: 0,
rotationCy: 0
};
};
/**
* Function: format
*
* Rounds all numbers to integers.
*/
mxAbstractCanvas2D.prototype.format = function(value)
{
return Math.round(parseFloat(value));
};
/**
* Function: addOp
*
* Adds the given operation to the path.
*/
mxAbstractCanvas2D.prototype.addOp = function()
{
if (this.path != null)
{
this.path.push(arguments[0]);
if (arguments.length > 2)
{
var s = this.state;
for (var i = 2; i < arguments.length; i += 2)
{
this.lastX = arguments[i - 1];
this.lastY = arguments[i];
this.path.push(this.format((this.lastX + s.dx) * s.scale));
this.path.push(this.format((this.lastY + s.dy) * s.scale));
}
}
}
};
/**
* Function: rotatePoint
*
* Rotates the given point and returns the result as an <mxPoint>.
*/
mxAbstractCanvas2D.prototype.rotatePoint = function(x, y, theta, cx, cy)
{
var rad = theta * (Math.PI / 180);
return mxUtils.getRotatedPoint(new mxPoint(x, y), Math.cos(rad),
Math.sin(rad), new mxPoint(cx, cy));
};
/**
* Function: save
*
* Saves the current state.
*/
mxAbstractCanvas2D.prototype.save = function()
{
this.states.push(this.state);
this.state = mxUtils.clone(this.state);
};
/**
* Function: restore
*
* Restores the current state.
*/
mxAbstractCanvas2D.prototype.restore = function()
{
if (this.states.length > 0)
{
this.state = this.states.pop();
}
};
/**
* Function: setLink
*
* Sets the current link. Hook for subclassers.
*/
mxAbstractCanvas2D.prototype.setLink = function(link)
{
// nop
};
/**
* Function: scale
*
* Scales the current state.
*/
mxAbstractCanvas2D.prototype.scale = function(value)
{
this.state.scale *= value;
this.state.strokeWidth *= value;
};
/**
* Function: translate
*
* Translates the current state.
*/
mxAbstractCanvas2D.prototype.translate = function(dx, dy)
{
this.state.dx += dx;
this.state.dy += dy;
};
/**
* Function: rotate
*
* Rotates the current state.
*/
mxAbstractCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
{
// nop
};
/**
* Function: setAlpha
*
* Sets the current alpha.
*/
mxAbstractCanvas2D.prototype.setAlpha = function(value)
{
this.state.alpha = value;
};
/**
* Function: setFillAlpha
*
* Sets the current solid fill alpha.
*/
mxAbstractCanvas2D.prototype.setFillAlpha = function(value)
{
this.state.fillAlpha = value;
};
/**
* Function: setStrokeAlpha
*
* Sets the current stroke alpha.
*/
mxAbstractCanvas2D.prototype.setStrokeAlpha = function(value)
{
this.state.strokeAlpha = value;
};
/**
* Function: setFillColor
*
* Sets the current fill color.
*/
mxAbstractCanvas2D.prototype.setFillColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.fillColor = value;
this.state.gradientColor = null;
};
/**
* Function: setGradient
*
* Sets the current gradient.
*/
mxAbstractCanvas2D.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2)
{
var s = this.state;
s.fillColor = color1;
s.gradientFillAlpha = (alpha1 != null) ? alpha1 : 1;
s.gradientColor = color2;
s.gradientAlpha = (alpha2 != null) ? alpha2 : 1;
s.gradientDirection = direction;
};
/**
* Function: setStrokeColor
*
* Sets the current stroke color.
*/
mxAbstractCanvas2D.prototype.setStrokeColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.strokeColor = value;
};
/**
* Function: setStrokeWidth
*
* Sets the current stroke width.
*/
mxAbstractCanvas2D.prototype.setStrokeWidth = function(value)
{
this.state.strokeWidth = value;
};
/**
* Function: setDashed
*
* Enables or disables dashed lines.
*/
mxAbstractCanvas2D.prototype.setDashed = function(value, fixDash)
{
this.state.dashed = value;
this.state.fixDash = fixDash;
};
/**
* Function: setDashPattern
*
* Sets the current dash pattern.
*/
mxAbstractCanvas2D.prototype.setDashPattern = function(value)
{
this.state.dashPattern = value;
};
/**
* Function: setLineCap
*
* Sets the current line cap.
*/
mxAbstractCanvas2D.prototype.setLineCap = function(value)
{
this.state.lineCap = value;
};
/**
* Function: setLineJoin
*
* Sets the current line join.
*/
mxAbstractCanvas2D.prototype.setLineJoin = function(value)
{
this.state.lineJoin = value;
};
/**
* Function: setMiterLimit
*
* Sets the current miter limit.
*/
mxAbstractCanvas2D.prototype.setMiterLimit = function(value)
{
this.state.miterLimit = value;
};
/**
* Function: setFontColor
*
* Sets the current font color.
*/
mxAbstractCanvas2D.prototype.setFontColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.fontColor = value;
};
/**
* Function: setFontColor
*
* Sets the current font color.
*/
mxAbstractCanvas2D.prototype.setFontBackgroundColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.fontBackgroundColor = value;
};
/**
* Function: setFontColor
*
* Sets the current font color.
*/
mxAbstractCanvas2D.prototype.setFontBorderColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.fontBorderColor = value;
};
/**
* Function: setFontSize
*
* Sets the current font size.
*/
mxAbstractCanvas2D.prototype.setFontSize = function(value)
{
this.state.fontSize = parseFloat(value);
};
/**
* Function: setFontFamily
*
* Sets the current font family.
*/
mxAbstractCanvas2D.prototype.setFontFamily = function(value)
{
this.state.fontFamily = value;
};
/**
* Function: setFontStyle
*
* Sets the current font style.
*/
mxAbstractCanvas2D.prototype.setFontStyle = function(value)
{
if (value == null)
{
value = 0;
}
this.state.fontStyle = value;
};
/**
* Function: setShadow
*
* Enables or disables and configures the current shadow.
*/
mxAbstractCanvas2D.prototype.setShadow = function(enabled)
{
this.state.shadow = enabled;
};
/**
* Function: setShadowColor
*
* Enables or disables and configures the current shadow.
*/
mxAbstractCanvas2D.prototype.setShadowColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
this.state.shadowColor = value;
};
/**
* Function: setShadowAlpha
*
* Enables or disables and configures the current shadow.
*/
mxAbstractCanvas2D.prototype.setShadowAlpha = function(value)
{
this.state.shadowAlpha = value;
};
/**
* Function: setShadowOffset
*
* Enables or disables and configures the current shadow.
*/
mxAbstractCanvas2D.prototype.setShadowOffset = function(dx, dy)
{
this.state.shadowDx = dx;
this.state.shadowDy = dy;
};
/**
* Function: begin
*
* Starts a new path.
*/
mxAbstractCanvas2D.prototype.begin = function()
{
this.lastX = 0;
this.lastY = 0;
this.path = [];
};
/**
* Function: moveTo
*
* Moves the current path the given coordinates.
*/
mxAbstractCanvas2D.prototype.moveTo = function(x, y)
{
this.addOp(this.moveOp, x, y);
};
/**
* Function: lineTo
*
* Draws a line to the given coordinates. Uses moveTo with the op argument.
*/
mxAbstractCanvas2D.prototype.lineTo = function(x, y)
{
this.addOp(this.lineOp, x, y);
};
/**
* Function: quadTo
*
* Adds a quadratic curve to the current path.
*/
mxAbstractCanvas2D.prototype.quadTo = function(x1, y1, x2, y2)
{
this.addOp(this.quadOp, x1, y1, x2, y2);
};
/**
* Function: curveTo
*
* Adds a bezier curve to the current path.
*/
mxAbstractCanvas2D.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
{
this.addOp(this.curveOp, x1, y1, x2, y2, x3, y3);
};
/**
* Function: arcTo
*
* Adds the given arc to the current path. This is a synthetic operation that
* is broken down into curves.
*/
mxAbstractCanvas2D.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y)
{
var curves = mxUtils.arcToCurves(this.lastX, this.lastY, rx, ry, angle, largeArcFlag, sweepFlag, x, y);
if (curves != null)
{
for (var i = 0; i < curves.length; i += 6)
{
this.curveTo(curves[i], curves[i + 1], curves[i + 2],
curves[i + 3], curves[i + 4], curves[i + 5]);
}
}
};
/**
* Function: close
*
* Closes the current path.
*/
mxAbstractCanvas2D.prototype.close = function(x1, y1, x2, y2, x3, y3)
{
this.addOp(this.closeOp);
};
/**
* Function: end
*
* Empty implementation for backwards compatibility. This will be removed.
*/
mxAbstractCanvas2D.prototype.end = function() { };
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxXmlCanvas2D
*
* Base class for all canvases. The following methods make up the public
* interface of the canvas 2D for all painting in mxGraph:
*
* - <save>, <restore>
* - <scale>, <translate>, <rotate>
* - <setAlpha>, <setFillAlpha>, <setStrokeAlpha>, <setFillColor>, <setGradient>,
* <setStrokeColor>, <setStrokeWidth>, <setDashed>, <setDashPattern>, <setLineCap>,
* <setLineJoin>, <setMiterLimit>
* - <setFontColor>, <setFontBackgroundColor>, <setFontBorderColor>, <setFontSize>,
* <setFontFamily>, <setFontStyle>
* - <setShadow>, <setShadowColor>, <setShadowAlpha>, <setShadowOffset>
* - <rect>, <roundrect>, <ellipse>, <image>, <text>
* - <begin>, <moveTo>, <lineTo>, <quadTo>, <curveTo>
* - <stroke>, <fill>, <fillAndStroke>
*
* <mxAbstractCanvas2D.arcTo> is an additional method for drawing paths. This is
* a synthetic method, meaning that it is turned into a sequence of curves by
* default. Subclassers may add native support for arcs.
*
* Constructor: mxXmlCanvas2D
*
* Constructs a new abstract canvas.
*/
function mxXmlCanvas2D(root)
{
mxAbstractCanvas2D.call(this);
/**
* Variable: root
*
* Reference to the container for the SVG content.
*/
this.root = root;
// Writes default settings;
this.writeDefaults();
};
/**
* Extends mxAbstractCanvas2D
*/
mxUtils.extend(mxXmlCanvas2D, mxAbstractCanvas2D);
/**
* Variable: textEnabled
*
* Specifies if text output should be enabled. Default is true.
*/
mxXmlCanvas2D.prototype.textEnabled = true;
/**
* Variable: compressed
*
* Specifies if the output should be compressed by removing redundant calls.
* Default is true.
*/
mxXmlCanvas2D.prototype.compressed = true;
/**
* Function: writeDefaults
*
* Writes the rendering defaults to <root>:
*/
mxXmlCanvas2D.prototype.writeDefaults = function()
{
var elem;
// Writes font defaults
elem = this.createElement('fontfamily');
elem.setAttribute('family', mxConstants.DEFAULT_FONTFAMILY);
this.root.appendChild(elem);
elem = this.createElement('fontsize');
elem.setAttribute('size', mxConstants.DEFAULT_FONTSIZE);
this.root.appendChild(elem);
// Writes shadow defaults
elem = this.createElement('shadowcolor');
elem.setAttribute('color', mxConstants.SHADOWCOLOR);
this.root.appendChild(elem);
elem = this.createElement('shadowalpha');
elem.setAttribute('alpha', mxConstants.SHADOW_OPACITY);
this.root.appendChild(elem);
elem = this.createElement('shadowoffset');
elem.setAttribute('dx', mxConstants.SHADOW_OFFSET_X);
elem.setAttribute('dy', mxConstants.SHADOW_OFFSET_Y);
this.root.appendChild(elem);
};
/**
* Function: format
*
* Returns a formatted number with 2 decimal places.
*/
mxXmlCanvas2D.prototype.format = function(value)
{
return parseFloat(parseFloat(value).toFixed(2));
};
/**
* Function: createElement
*
* Creates the given element using the owner document of <root>.
*/
mxXmlCanvas2D.prototype.createElement = function(name)
{
return this.root.ownerDocument.createElement(name);
};
/**
* Function: save
*
* Saves the drawing state.
*/
mxXmlCanvas2D.prototype.save = function()
{
if (this.compressed)
{
mxAbstractCanvas2D.prototype.save.apply(this, arguments);
}
this.root.appendChild(this.createElement('save'));
};
/**
* Function: restore
*
* Restores the drawing state.
*/
mxXmlCanvas2D.prototype.restore = function()
{
if (this.compressed)
{
mxAbstractCanvas2D.prototype.restore.apply(this, arguments);
}
this.root.appendChild(this.createElement('restore'));
};
/**
* Function: scale
*
* Scales the output.
*
* Parameters:
*
* scale - Number that represents the scale where 1 is equal to 100%.
*/
mxXmlCanvas2D.prototype.scale = function(value)
{
var elem = this.createElement('scale');
elem.setAttribute('scale', value);
this.root.appendChild(elem);
};
/**
* Function: translate
*
* Translates the output.
*
* Parameters:
*
* dx - Number that specifies the horizontal translation.
* dy - Number that specifies the vertical translation.
*/
mxXmlCanvas2D.prototype.translate = function(dx, dy)
{
var elem = this.createElement('translate');
elem.setAttribute('dx', this.format(dx));
elem.setAttribute('dy', this.format(dy));
this.root.appendChild(elem);
};
/**
* Function: rotate
*
* Rotates and/or flips the output around a given center. (Note: Due to
* limitations in VML, the rotation cannot be concatenated.)
*
* Parameters:
*
* theta - Number that represents the angle of the rotation (in degrees).
* flipH - Boolean indicating if the output should be flipped horizontally.
* flipV - Boolean indicating if the output should be flipped vertically.
* cx - Number that represents the x-coordinate of the rotation center.
* cy - Number that represents the y-coordinate of the rotation center.
*/
mxXmlCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
{
var elem = this.createElement('rotate');
if (theta != 0 || flipH || flipV)
{
elem.setAttribute('theta', this.format(theta));
elem.setAttribute('flipH', (flipH) ? '1' : '0');
elem.setAttribute('flipV', (flipV) ? '1' : '0');
elem.setAttribute('cx', this.format(cx));
elem.setAttribute('cy', this.format(cy));
this.root.appendChild(elem);
}
};
/**
* Function: setAlpha
*
* Sets the current alpha.
*
* Parameters:
*
* value - Number that represents the new alpha. Possible values are between
* 1 (opaque) and 0 (transparent).
*/
mxXmlCanvas2D.prototype.setAlpha = function(value)
{
if (this.compressed)
{
if (this.state.alpha == value)
{
return;
}
mxAbstractCanvas2D.prototype.setAlpha.apply(this, arguments);
}
var elem = this.createElement('alpha');
elem.setAttribute('alpha', this.format(value));
this.root.appendChild(elem);
};
/**
* Function: setFillAlpha
*
* Sets the current fill alpha.
*
* Parameters:
*
* value - Number that represents the new fill alpha. Possible values are between
* 1 (opaque) and 0 (transparent).
*/
mxXmlCanvas2D.prototype.setFillAlpha = function(value)
{
if (this.compressed)
{
if (this.state.fillAlpha == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFillAlpha.apply(this, arguments);
}
var elem = this.createElement('fillalpha');
elem.setAttribute('alpha', this.format(value));
this.root.appendChild(elem);
};
/**
* Function: setStrokeAlpha
*
* Sets the current stroke alpha.
*
* Parameters:
*
* value - Number that represents the new stroke alpha. Possible values are between
* 1 (opaque) and 0 (transparent).
*/
mxXmlCanvas2D.prototype.setStrokeAlpha = function(value)
{
if (this.compressed)
{
if (this.state.strokeAlpha == value)
{
return;
}
mxAbstractCanvas2D.prototype.setStrokeAlpha.apply(this, arguments);
}
var elem = this.createElement('strokealpha');
elem.setAttribute('alpha', this.format(value));
this.root.appendChild(elem);
};
/**
* Function: setFillColor
*
* Sets the current fill color.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setFillColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.compressed)
{
if (this.state.fillColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFillColor.apply(this, arguments);
}
var elem = this.createElement('fillcolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
};
/**
* Function: setGradient
*
* Sets the gradient. Note that the coordinates may be ignored by some implementations.
*
* Parameters:
*
* color1 - Hexadecimal representation of the start color.
* color2 - Hexadecimal representation of the end color.
* x - X-coordinate of the gradient region.
* y - y-coordinate of the gradient region.
* w - Width of the gradient region.
* h - Height of the gradient region.
* direction - One of <mxConstants.DIRECTION_NORTH>, <mxConstants.DIRECTION_EAST>,
* <mxConstants.DIRECTION_SOUTH> or <mxConstants.DIRECTION_WEST>.
* alpha1 - Optional alpha of the start color. Default is 1. Possible values
* are between 1 (opaque) and 0 (transparent).
* alpha2 - Optional alpha of the end color. Default is 1. Possible values
* are between 1 (opaque) and 0 (transparent).
*/
mxXmlCanvas2D.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2)
{
if (color1 != null && color2 != null)
{
mxAbstractCanvas2D.prototype.setGradient.apply(this, arguments);
var elem = this.createElement('gradient');
elem.setAttribute('c1', color1);
elem.setAttribute('c2', color2);
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
// Default direction is south
if (direction != null)
{
elem.setAttribute('direction', direction);
}
if (alpha1 != null)
{
elem.setAttribute('alpha1', alpha1);
}
if (alpha2 != null)
{
elem.setAttribute('alpha2', alpha2);
}
this.root.appendChild(elem);
}
};
/**
* Function: setStrokeColor
*
* Sets the current stroke color.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setStrokeColor = function(value)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.compressed)
{
if (this.state.strokeColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setStrokeColor.apply(this, arguments);
}
var elem = this.createElement('strokecolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
};
/**
* Function: setStrokeWidth
*
* Sets the current stroke width.
*
* Parameters:
*
* value - Numeric representation of the stroke width.
*/
mxXmlCanvas2D.prototype.setStrokeWidth = function(value)
{
if (this.compressed)
{
if (this.state.strokeWidth == value)
{
return;
}
mxAbstractCanvas2D.prototype.setStrokeWidth.apply(this, arguments);
}
var elem = this.createElement('strokewidth');
elem.setAttribute('width', this.format(value));
this.root.appendChild(elem);
};
/**
* Function: setDashed
*
* Enables or disables dashed lines.
*
* Parameters:
*
* value - Boolean that specifies if dashed lines should be enabled.
* value - Boolean that specifies if the stroke width should be ignored
* for the dash pattern. Default is false.
*/
mxXmlCanvas2D.prototype.setDashed = function(value, fixDash)
{
if (this.compressed)
{
if (this.state.dashed == value)
{
return;
}
mxAbstractCanvas2D.prototype.setDashed.apply(this, arguments);
}
var elem = this.createElement('dashed');
elem.setAttribute('dashed', (value) ? '1' : '0');
if (fixDash != null)
{
elem.setAttribute('fixDash', (fixDash) ? '1' : '0');
}
this.root.appendChild(elem);
};
/**
* Function: setDashPattern
*
* Sets the current dash pattern. Default is '3 3'.
*
* Parameters:
*
* value - String that represents the dash pattern, which is a sequence of
* numbers defining the length of the dashes and the length of the spaces
* between the dashes. The lengths are relative to the line width - a length
* of 1 is equals to the line width.
*/
mxXmlCanvas2D.prototype.setDashPattern = function(value)
{
if (this.compressed)
{
if (this.state.dashPattern == value)
{
return;
}
mxAbstractCanvas2D.prototype.setDashPattern.apply(this, arguments);
}
var elem = this.createElement('dashpattern');
elem.setAttribute('pattern', value);
this.root.appendChild(elem);
};
/**
* Function: setLineCap
*
* Sets the line cap. Default is 'flat' which corresponds to 'butt' in SVG.
*
* Parameters:
*
* value - String that represents the line cap. Possible values are flat, round
* and square.
*/
mxXmlCanvas2D.prototype.setLineCap = function(value)
{
if (this.compressed)
{
if (this.state.lineCap == value)
{
return;
}
mxAbstractCanvas2D.prototype.setLineCap.apply(this, arguments);
}
var elem = this.createElement('linecap');
elem.setAttribute('cap', value);
this.root.appendChild(elem);
};
/**
* Function: setLineJoin
*
* Sets the line join. Default is 'miter'.
*
* Parameters:
*
* value - String that represents the line join. Possible values are miter,
* round and bevel.
*/
mxXmlCanvas2D.prototype.setLineJoin = function(value)
{
if (this.compressed)
{
if (this.state.lineJoin == value)
{
return;
}
mxAbstractCanvas2D.prototype.setLineJoin.apply(this, arguments);
}
var elem = this.createElement('linejoin');
elem.setAttribute('join', value);
this.root.appendChild(elem);
};
/**
* Function: setMiterLimit
*
* Sets the miter limit. Default is 10.
*
* Parameters:
*
* value - Number that represents the miter limit.
*/
mxXmlCanvas2D.prototype.setMiterLimit = function(value)
{
if (this.compressed)
{
if (this.state.miterLimit == value)
{
return;
}
mxAbstractCanvas2D.prototype.setMiterLimit.apply(this, arguments);
}
var elem = this.createElement('miterlimit');
elem.setAttribute('limit', value);
this.root.appendChild(elem);
};
/**
* Function: setFontColor
*
* Sets the current font color. Default is '#000000'.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setFontColor = function(value)
{
if (this.textEnabled)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.compressed)
{
if (this.state.fontColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontColor.apply(this, arguments);
}
var elem = this.createElement('fontcolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
}
};
/**
* Function: setFontBackgroundColor
*
* Sets the current font background color.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setFontBackgroundColor = function(value)
{
if (this.textEnabled)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.compressed)
{
if (this.state.fontBackgroundColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontBackgroundColor.apply(this, arguments);
}
var elem = this.createElement('fontbackgroundcolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
}
};
/**
* Function: setFontBorderColor
*
* Sets the current font border color.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setFontBorderColor = function(value)
{
if (this.textEnabled)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.compressed)
{
if (this.state.fontBorderColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontBorderColor.apply(this, arguments);
}
var elem = this.createElement('fontbordercolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
}
};
/**
* Function: setFontSize
*
* Sets the current font size. Default is <mxConstants.DEFAULT_FONTSIZE>.
*
* Parameters:
*
* value - Numeric representation of the font size.
*/
mxXmlCanvas2D.prototype.setFontSize = function(value)
{
if (this.textEnabled)
{
if (this.compressed)
{
if (this.state.fontSize == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontSize.apply(this, arguments);
}
var elem = this.createElement('fontsize');
elem.setAttribute('size', value);
this.root.appendChild(elem);
}
};
/**
* Function: setFontFamily
*
* Sets the current font family. Default is <mxConstants.DEFAULT_FONTFAMILY>.
*
* Parameters:
*
* value - String representation of the font family. This handles the same
* values as the CSS font-family property.
*/
mxXmlCanvas2D.prototype.setFontFamily = function(value)
{
if (this.textEnabled)
{
if (this.compressed)
{
if (this.state.fontFamily == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontFamily.apply(this, arguments);
}
var elem = this.createElement('fontfamily');
elem.setAttribute('family', value);
this.root.appendChild(elem);
}
};
/**
* Function: setFontStyle
*
* Sets the current font style.
*
* Parameters:
*
* value - Numeric representation of the font family. This is the sum of the
* font styles from <mxConstants>.
*/
mxXmlCanvas2D.prototype.setFontStyle = function(value)
{
if (this.textEnabled)
{
if (value == null)
{
value = 0;
}
if (this.compressed)
{
if (this.state.fontStyle == value)
{
return;
}
mxAbstractCanvas2D.prototype.setFontStyle.apply(this, arguments);
}
var elem = this.createElement('fontstyle');
elem.setAttribute('style', value);
this.root.appendChild(elem);
}
};
/**
* Function: setShadow
*
* Enables or disables shadows.
*
* Parameters:
*
* value - Boolean that specifies if shadows should be enabled.
*/
mxXmlCanvas2D.prototype.setShadow = function(value)
{
if (this.compressed)
{
if (this.state.shadow == value)
{
return;
}
mxAbstractCanvas2D.prototype.setShadow.apply(this, arguments);
}
var elem = this.createElement('shadow');
elem.setAttribute('enabled', (value) ? '1' : '0');
this.root.appendChild(elem);
};
/**
* Function: setShadowColor
*
* Sets the current shadow color. Default is <mxConstants.SHADOWCOLOR>.
*
* Parameters:
*
* value - Hexadecimal representation of the color or 'none'.
*/
mxXmlCanvas2D.prototype.setShadowColor = function(value)
{
if (this.compressed)
{
if (value == mxConstants.NONE)
{
value = null;
}
if (this.state.shadowColor == value)
{
return;
}
mxAbstractCanvas2D.prototype.setShadowColor.apply(this, arguments);
}
var elem = this.createElement('shadowcolor');
elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
this.root.appendChild(elem);
};
/**
* Function: setShadowAlpha
*
* Sets the current shadows alpha. Default is <mxConstants.SHADOW_OPACITY>.
*
* Parameters:
*
* value - Number that represents the new alpha. Possible values are between
* 1 (opaque) and 0 (transparent).
*/
mxXmlCanvas2D.prototype.setShadowAlpha = function(value)
{
if (this.compressed)
{
if (this.state.shadowAlpha == value)
{
return;
}
mxAbstractCanvas2D.prototype.setShadowAlpha.apply(this, arguments);
}
var elem = this.createElement('shadowalpha');
elem.setAttribute('alpha', value);
this.root.appendChild(elem);
};
/**
* Function: setShadowOffset
*
* Sets the current shadow offset.
*
* Parameters:
*
* dx - Number that represents the horizontal offset of the shadow.
* dy - Number that represents the vertical offset of the shadow.
*/
mxXmlCanvas2D.prototype.setShadowOffset = function(dx, dy)
{
if (this.compressed)
{
if (this.state.shadowDx == dx && this.state.shadowDy == dy)
{
return;
}
mxAbstractCanvas2D.prototype.setShadowOffset.apply(this, arguments);
}
var elem = this.createElement('shadowoffset');
elem.setAttribute('dx', dx);
elem.setAttribute('dy', dy);
this.root.appendChild(elem);
};
/**
* Function: rect
*
* Puts a rectangle into the drawing buffer.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the rectangle.
* y - Number that represents the y-coordinate of the rectangle.
* w - Number that represents the width of the rectangle.
* h - Number that represents the height of the rectangle.
*/
mxXmlCanvas2D.prototype.rect = function(x, y, w, h)
{
var elem = this.createElement('rect');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
this.root.appendChild(elem);
};
/**
* Function: roundrect
*
* Puts a rounded rectangle into the drawing buffer.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the rectangle.
* y - Number that represents the y-coordinate of the rectangle.
* w - Number that represents the width of the rectangle.
* h - Number that represents the height of the rectangle.
* dx - Number that represents the horizontal rounding.
* dy - Number that represents the vertical rounding.
*/
mxXmlCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy)
{
var elem = this.createElement('roundrect');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
elem.setAttribute('dx', this.format(dx));
elem.setAttribute('dy', this.format(dy));
this.root.appendChild(elem);
};
/**
* Function: ellipse
*
* Puts an ellipse into the drawing buffer.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the ellipse.
* y - Number that represents the y-coordinate of the ellipse.
* w - Number that represents the width of the ellipse.
* h - Number that represents the height of the ellipse.
*/
mxXmlCanvas2D.prototype.ellipse = function(x, y, w, h)
{
var elem = this.createElement('ellipse');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
this.root.appendChild(elem);
};
/**
* Function: image
*
* Paints an image.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the image.
* y - Number that represents the y-coordinate of the image.
* w - Number that represents the width of the image.
* h - Number that represents the height of the image.
* src - String that specifies the URL of the image.
* aspect - Boolean indicating if the aspect of the image should be preserved.
* flipH - Boolean indicating if the image should be flipped horizontally.
* flipV - Boolean indicating if the image should be flipped vertically.
*/
mxXmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
{
src = this.converter.convert(src);
// LATER: Add option for embedding images as base64.
var elem = this.createElement('image');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
elem.setAttribute('src', src);
elem.setAttribute('aspect', (aspect) ? '1' : '0');
elem.setAttribute('flipH', (flipH) ? '1' : '0');
elem.setAttribute('flipV', (flipV) ? '1' : '0');
this.root.appendChild(elem);
};
/**
* Function: begin
*
* Starts a new path and puts it into the drawing buffer.
*/
mxXmlCanvas2D.prototype.begin = function()
{
this.root.appendChild(this.createElement('begin'));
this.lastX = 0;
this.lastY = 0;
};
/**
* Function: moveTo
*
* Moves the current path the given point.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the point.
* y - Number that represents the y-coordinate of the point.
*/
mxXmlCanvas2D.prototype.moveTo = function(x, y)
{
var elem = this.createElement('move');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
this.root.appendChild(elem);
this.lastX = x;
this.lastY = y;
};
/**
* Function: lineTo
*
* Draws a line to the given coordinates.
*
* Parameters:
*
* x - Number that represents the x-coordinate of the endpoint.
* y - Number that represents the y-coordinate of the endpoint.
*/
mxXmlCanvas2D.prototype.lineTo = function(x, y)
{
var elem = this.createElement('line');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
this.root.appendChild(elem);
this.lastX = x;
this.lastY = y;
};
/**
* Function: quadTo
*
* Adds a quadratic curve to the current path.
*
* Parameters:
*
* x1 - Number that represents the x-coordinate of the control point.
* y1 - Number that represents the y-coordinate of the control point.
* x2 - Number that represents the x-coordinate of the endpoint.
* y2 - Number that represents the y-coordinate of the endpoint.
*/
mxXmlCanvas2D.prototype.quadTo = function(x1, y1, x2, y2)
{
var elem = this.createElement('quad');
elem.setAttribute('x1', this.format(x1));
elem.setAttribute('y1', this.format(y1));
elem.setAttribute('x2', this.format(x2));
elem.setAttribute('y2', this.format(y2));
this.root.appendChild(elem);
this.lastX = x2;
this.lastY = y2;
};
/**
* Function: curveTo
*
* Adds a bezier curve to the current path.
*
* Parameters:
*
* x1 - Number that represents the x-coordinate of the first control point.
* y1 - Number that represents the y-coordinate of the first control point.
* x2 - Number that represents the x-coordinate of the second control point.
* y2 - Number that represents the y-coordinate of the second control point.
* x3 - Number that represents the x-coordinate of the endpoint.
* y3 - Number that represents the y-coordinate of the endpoint.
*/
mxXmlCanvas2D.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
{
var elem = this.createElement('curve');
elem.setAttribute('x1', this.format(x1));
elem.setAttribute('y1', this.format(y1));
elem.setAttribute('x2', this.format(x2));
elem.setAttribute('y2', this.format(y2));
elem.setAttribute('x3', this.format(x3));
elem.setAttribute('y3', this.format(y3));
this.root.appendChild(elem);
this.lastX = x3;
this.lastY = y3;
};
/**
* Function: close
*
* Closes the current path.
*/
mxXmlCanvas2D.prototype.close = function()
{
this.root.appendChild(this.createElement('close'));
};
/**
* Function: text
*
* Paints the given text. Possible values for format are empty string for
* plain text and html for HTML markup. Background and border color as well
* as clipping is not available in plain text labels for VML. HTML labels
* are not available as part of shapes with no foreignObject support in SVG
* (eg. IE9, IE10).
*
* Parameters:
*
* x - Number that represents the x-coordinate of the text.
* y - Number that represents the y-coordinate of the text.
* w - Number that represents the available width for the text or 0 for automatic width.
* h - Number that represents the available height for the text or 0 for automatic height.
* str - String that specifies the text to be painted.
* align - String that represents the horizontal alignment.
* valign - String that represents the vertical alignment.
* wrap - Boolean that specifies if word-wrapping is enabled. Requires w > 0.
* format - Empty string for plain text or 'html' for HTML markup.
* overflow - Specifies the overflow behaviour of the label. Requires w > 0 and/or h > 0.
* clip - Boolean that specifies if the label should be clipped. Requires w > 0 and/or h > 0.
* rotation - Number that specifies the angle of the rotation around the anchor point of the text.
* dir - Optional string that specifies the text direction. Possible values are rtl and lrt.
*/
mxXmlCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)
{
if (this.textEnabled && str != null)
{
if (mxUtils.isNode(str))
{
str = mxUtils.getOuterHtml(str);
}
var elem = this.createElement('text');
elem.setAttribute('x', this.format(x));
elem.setAttribute('y', this.format(y));
elem.setAttribute('w', this.format(w));
elem.setAttribute('h', this.format(h));
elem.setAttribute('str', str);
if (align != null)
{
elem.setAttribute('align', align);
}
if (valign != null)
{
elem.setAttribute('valign', valign);
}
elem.setAttribute('wrap', (wrap) ? '1' : '0');
if (format == null)
{
format = '';
}
elem.setAttribute('format', format);
if (overflow != null)
{
elem.setAttribute('overflow', overflow);
}
if (clip != null)
{
elem.setAttribute('clip', (clip) ? '1' : '0');
}
if (rotation != null)
{
elem.setAttribute('rotation', rotation);
}
if (dir != null)
{
elem.setAttribute('dir', dir);
}
this.root.appendChild(elem);
}
};
/**
* Function: stroke
*
* Paints the outline of the current drawing buffer.
*/
mxXmlCanvas2D.prototype.stroke = function()
{
this.root.appendChild(this.createElement('stroke'));
};
/**
* Function: fill
*
* Fills the current drawing buffer.
*/
mxXmlCanvas2D.prototype.fill = function()
{
this.root.appendChild(this.createElement('fill'));
};
/**
* Function: fillAndStroke
*
* Fills the current drawing buffer and its outline.
*/
mxXmlCanvas2D.prototype.fillAndStroke = function()
{
this.root.appendChild(this.createElement('fillstroke'));
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSvgCanvas2D
*
* Extends <mxAbstractCanvas2D> to implement a canvas for SVG. This canvas writes all
* calls as SVG output to the given SVG root node.
*
* (code)
* var svgDoc = mxUtils.createXmlDocument();
* var root = (svgDoc.createElementNS != null) ?
* svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg');
*
* if (svgDoc.createElementNS == null)
* {
* root.setAttribute('xmlns', mxConstants.NS_SVG);
* root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
* }
* else
* {
* root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
* }
*
* var bounds = graph.getGraphBounds();
* root.setAttribute('width', (bounds.x + bounds.width + 4) + 'px');
* root.setAttribute('height', (bounds.y + bounds.height + 4) + 'px');
* root.setAttribute('version', '1.1');
*
* svgDoc.appendChild(root);
*
* var svgCanvas = new mxSvgCanvas2D(root);
* (end)
*
* A description of the public API is available in <mxXmlCanvas2D>.
*
* To disable anti-aliasing in the output, use the following code.
*
* (code)
* graph.view.canvas.ownerSVGElement.setAttribute('shape-rendering', 'crispEdges');
* (end)
*
* Or set the respective attribute in the SVG element directly.
*
* Constructor: mxSvgCanvas2D
*
* Constructs a new SVG canvas.
*
* Parameters:
*
* root - SVG container for the output.
* styleEnabled - Optional boolean that specifies if a style section should be
* added. The style section sets the default font-size, font-family and
* stroke-miterlimit globally. Default is false.
*/
function mxSvgCanvas2D(root, styleEnabled)
{
mxAbstractCanvas2D.call(this);
/**
* Variable: root
*
* Reference to the container for the SVG content.
*/
this.root = root;
/**
* Variable: gradients
*
* Local cache of gradients for quick lookups.
*/
this.gradients = [];
/**
* Variable: defs
*
* Reference to the defs section of the SVG document. Only for export.
*/
this.defs = null;
/**
* Variable: styleEnabled
*
* Stores the value of styleEnabled passed to the constructor.
*/
this.styleEnabled = (styleEnabled != null) ? styleEnabled : false;
var svg = null;
// Adds optional defs section for export
if (root.ownerDocument != document)
{
var node = root;
// Finds owner SVG element in XML DOM
while (node != null && node.nodeName != 'svg')
{
node = node.parentNode;
}
svg = node;
}
if (svg != null)
{
// Tries to get existing defs section
var tmp = svg.getElementsByTagName('defs');
if (tmp.length > 0)
{
this.defs = svg.getElementsByTagName('defs')[0];
}
// Adds defs section if none exists
if (this.defs == null)
{
this.defs = this.createElement('defs');
if (svg.firstChild != null)
{
svg.insertBefore(this.defs, svg.firstChild);
}
else
{
svg.appendChild(this.defs);
}
}
// Adds stylesheet
if (this.styleEnabled)
{
this.defs.appendChild(this.createStyle());
}
}
};
/**
* Extends mxAbstractCanvas2D
*/
mxUtils.extend(mxSvgCanvas2D, mxAbstractCanvas2D);
/**
* Capability check for DOM parser.
*/
(function()
{
mxSvgCanvas2D.prototype.useDomParser = !mxClient.IS_IE && typeof DOMParser === 'function' && typeof XMLSerializer === 'function';
if (mxSvgCanvas2D.prototype.useDomParser)
{
// Checks using a generic test text if the parsing actually works. This is a workaround
// for older browsers where the capability check returns true but the parsing fails.
try
{
var doc = new DOMParser().parseFromString('test text', 'text/html');
mxSvgCanvas2D.prototype.useDomParser = doc != null;
}
catch (e)
{
mxSvgCanvas2D.prototype.useDomParser = false;
}
}
})();
/**
* Variable: path
*
* Holds the current DOM node.
*/
mxSvgCanvas2D.prototype.node = null;
/**
* Variable: matchHtmlAlignment
*
* Specifies if plain text output should match the vertical HTML alignment.
* Defaul is true.
*/
mxSvgCanvas2D.prototype.matchHtmlAlignment = true;
/**
* Variable: textEnabled
*
* Specifies if text output should be enabled. Default is true.
*/
mxSvgCanvas2D.prototype.textEnabled = true;
/**
* Variable: foEnabled
*
* Specifies if use of foreignObject for HTML markup is allowed. Default is true.
*/
mxSvgCanvas2D.prototype.foEnabled = true;
/**
* Variable: foAltText
*
* Specifies the fallback text for unsupported foreignObjects in exported
* documents. Default is '[Object]'. If this is set to null then no fallback
* text is added to the exported document.
*/
mxSvgCanvas2D.prototype.foAltText = '[Object]';
/**
* Variable: foOffset
*
* Offset to be used for foreignObjects.
*/
mxSvgCanvas2D.prototype.foOffset = 0;
/**
* Variable: textOffset
*
* Offset to be used for text elements.
*/
mxSvgCanvas2D.prototype.textOffset = 0;
/**
* Variable: imageOffset
*
* Offset to be used for image elements.
*/
mxSvgCanvas2D.prototype.imageOffset = 0;
/**
* Variable: strokeTolerance
*
* Adds transparent paths for strokes.
*/
mxSvgCanvas2D.prototype.strokeTolerance = 0;
/**
* Variable: minStrokeWidth
*
* Minimum stroke width for output.
*/
mxSvgCanvas2D.prototype.minStrokeWidth = 1;
/**
* Variable: refCount
*
* Local counter for references in SVG export.
*/
mxSvgCanvas2D.prototype.refCount = 0;
/**
* Variable: blockImagePointerEvents
*
* Specifies if a transparent rectangle should be added on top of images to absorb
* all pointer events. Default is false. This is only needed in Firefox to disable
* control-clicks on images.
*/
mxSvgCanvas2D.prototype.blockImagePointerEvents = false;
/**
* Variable: lineHeightCorrection
*
* Correction factor for <mxConstants.LINE_HEIGHT> in HTML output. Default is 1.
*/
mxSvgCanvas2D.prototype.lineHeightCorrection = 1;
/**
* Variable: pointerEventsValue
*
* Default value for active pointer events. Default is all.
*/
mxSvgCanvas2D.prototype.pointerEventsValue = 'all';
/**
* Variable: fontMetricsPadding
*
* Padding to be added for text that is not wrapped to account for differences
* in font metrics on different platforms in pixels. Default is 10.
*/
mxSvgCanvas2D.prototype.fontMetricsPadding = 10;
/**
* Variable: cacheOffsetSize
*
* Specifies if offsetWidth and offsetHeight should be cached. Default is true.
* This is used to speed up repaint of text in <updateText>.
*/
mxSvgCanvas2D.prototype.cacheOffsetSize = true;
/**
* Function: format
*
* Rounds all numbers to 2 decimal points.
*/
mxSvgCanvas2D.prototype.format = function(value)
{
return parseFloat(parseFloat(value).toFixed(2));
};
/**
* Function: getBaseUrl
*
* Returns the URL of the page without the hash part. This needs to use href to
* include any search part with no params (ie question mark alone). This is a
* workaround for the fact that window.location.search is empty if there is
* no search string behind the question mark.
*/
mxSvgCanvas2D.prototype.getBaseUrl = function()
{
var href = window.location.href;
var hash = href.lastIndexOf('#');
if (hash > 0)
{
href = href.substring(0, hash);
}
return href;
};
/**
* Function: reset
*
* Returns any offsets for rendering pixels.
*/
mxSvgCanvas2D.prototype.reset = function()
{
mxAbstractCanvas2D.prototype.reset.apply(this, arguments);
this.gradients = [];
};
/**
* Function: createStyle
*
* Creates the optional style section.
*/
mxSvgCanvas2D.prototype.createStyle = function(x)
{
var style = this.createElement('style');
style.setAttribute('type', 'text/css');
mxUtils.write(style, 'svg{font-family:' + mxConstants.DEFAULT_FONTFAMILY +
';font-size:' + mxConstants.DEFAULT_FONTSIZE +
';fill:none;stroke-miterlimit:10}');
return style;
};
/**
* Function: createElement
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.createElement = function(tagName, namespace)
{
if (this.root.ownerDocument.createElementNS != null)
{
return this.root.ownerDocument.createElementNS(namespace || mxConstants.NS_SVG, tagName);
}
else
{
var elt = this.root.ownerDocument.createElement(tagName);
if (namespace != null)
{
elt.setAttribute('xmlns', namespace);
}
return elt;
}
};
/**
* Function: getAlternateContent
*
* Returns the alternate content for the given foreignObject.
*/
mxSvgCanvas2D.prototype.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
{
if (this.foAltText != null)
{
var s = this.state;
var alt = this.createElement('text');
alt.setAttribute('x', Math.round(w / 2));
alt.setAttribute('y', Math.round((h + s.fontSize) / 2));
alt.setAttribute('fill', s.fontColor || 'black');
alt.setAttribute('text-anchor', 'middle');
alt.setAttribute('font-size', s.fontSize + 'px');
alt.setAttribute('font-family', s.fontFamily);
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
alt.setAttribute('font-weight', 'bold');
}
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
alt.setAttribute('font-style', 'italic');
}
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
alt.setAttribute('text-decoration', 'underline');
}
mxUtils.write(alt, this.foAltText);
return alt;
}
else
{
return null;
}
};
/**
* Function: createGradientId
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.createGradientId = function(start, end, alpha1, alpha2, direction)
{
// Removes illegal characters from gradient ID
if (start.charAt(0) == '#')
{
start = start.substring(1);
}
if (end.charAt(0) == '#')
{
end = end.substring(1);
}
// Workaround for gradient IDs not working in Safari 5 / Chrome 6
// if they contain uppercase characters
start = start.toLowerCase() + '-' + alpha1;
end = end.toLowerCase() + '-' + alpha2;
// Wrong gradient directions possible?
var dir = null;
if (direction == null || direction == mxConstants.DIRECTION_SOUTH)
{
dir = 's';
}
else if (direction == mxConstants.DIRECTION_EAST)
{
dir = 'e';
}
else
{
var tmp = start;
start = end;
end = tmp;
if (direction == mxConstants.DIRECTION_NORTH)
{
dir = 's';
}
else if (direction == mxConstants.DIRECTION_WEST)
{
dir = 'e';
}
}
return 'mx-gradient-' + start + '-' + end + '-' + dir;
};
/**
* Function: getSvgGradient
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.getSvgGradient = function(start, end, alpha1, alpha2, direction)
{
var id = this.createGradientId(start, end, alpha1, alpha2, direction);
var gradient = this.gradients[id];
if (gradient == null)
{
var svg = this.root.ownerSVGElement;
var counter = 0;
var tmpId = id + '-' + counter;
if (svg != null)
{
gradient = svg.ownerDocument.getElementById(tmpId);
while (gradient != null && gradient.ownerSVGElement != svg)
{
tmpId = id + '-' + counter++;
gradient = svg.ownerDocument.getElementById(tmpId);
}
}
else
{
// Uses shorter IDs for export
tmpId = 'id' + (++this.refCount);
}
if (gradient == null)
{
gradient = this.createSvgGradient(start, end, alpha1, alpha2, direction);
gradient.setAttribute('id', tmpId);
if (this.defs != null)
{
this.defs.appendChild(gradient);
}
else
{
svg.appendChild(gradient);
}
}
this.gradients[id] = gradient;
}
return gradient.getAttribute('id');
};
/**
* Function: createSvgGradient
*
* Creates the given SVG gradient.
*/
mxSvgCanvas2D.prototype.createSvgGradient = function(start, end, alpha1, alpha2, direction)
{
var gradient = this.createElement('linearGradient');
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '0%');
gradient.setAttribute('y2', '0%');
if (direction == null || direction == mxConstants.DIRECTION_SOUTH)
{
gradient.setAttribute('y2', '100%');
}
else if (direction == mxConstants.DIRECTION_EAST)
{
gradient.setAttribute('x2', '100%');
}
else if (direction == mxConstants.DIRECTION_NORTH)
{
gradient.setAttribute('y1', '100%');
}
else if (direction == mxConstants.DIRECTION_WEST)
{
gradient.setAttribute('x1', '100%');
}
var op = (alpha1 < 1) ? ';stop-opacity:' + alpha1 : '';
var stop = this.createElement('stop');
stop.setAttribute('offset', '0%');
stop.setAttribute('style', 'stop-color:' + start + op);
gradient.appendChild(stop);
op = (alpha2 < 1) ? ';stop-opacity:' + alpha2 : '';
stop = this.createElement('stop');
stop.setAttribute('offset', '100%');
stop.setAttribute('style', 'stop-color:' + end + op);
gradient.appendChild(stop);
return gradient;
};
/**
* Function: addNode
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.addNode = function(filled, stroked)
{
var node = this.node;
var s = this.state;
if (node != null)
{
if (node.nodeName == 'path')
{
// Checks if the path is not empty
if (this.path != null && this.path.length > 0)
{
node.setAttribute('d', this.path.join(' '));
}
else
{
return;
}
}
if (filled && s.fillColor != null)
{
this.updateFill();
}
else if (!this.styleEnabled)
{
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=814952
if (node.nodeName == 'ellipse' && mxClient.IS_FF)
{
node.setAttribute('fill', 'transparent');
}
else
{
node.setAttribute('fill', 'none');
}
// Sets the actual filled state for stroke tolerance
filled = false;
}
if (stroked && s.strokeColor != null)
{
this.updateStroke();
}
else if (!this.styleEnabled)
{
node.setAttribute('stroke', 'none');
}
if (s.transform != null && s.transform.length > 0)
{
node.setAttribute('transform', s.transform);
}
if (s.shadow)
{
this.root.appendChild(this.createShadow(node));
}
// Adds stroke tolerance
if (this.strokeTolerance > 0 && !filled)
{
this.root.appendChild(this.createTolerance(node));
}
// Adds pointer events
if (this.pointerEvents)
{
node.setAttribute('pointer-events', this.pointerEventsValue);
}
// Enables clicks for nodes inside a link element
else if (!this.pointerEvents && this.originalRoot == null)
{
node.setAttribute('pointer-events', 'none');
}
// Removes invisible nodes from output if they don't handle events
if ((node.nodeName != 'rect' && node.nodeName != 'path' && node.nodeName != 'ellipse') ||
(node.getAttribute('fill') != 'none' && node.getAttribute('fill') != 'transparent') ||
node.getAttribute('stroke') != 'none' || node.getAttribute('pointer-events') != 'none')
{
// LATER: Update existing DOM for performance
this.root.appendChild(node);
}
this.node = null;
}
};
/**
* Function: updateFill
*
* Transfers the stroke attributes from <state> to <node>.
*/
mxSvgCanvas2D.prototype.updateFill = function()
{
var s = this.state;
if (s.alpha < 1 || s.fillAlpha < 1)
{
this.node.setAttribute('fill-opacity', s.alpha * s.fillAlpha);
}
if (s.fillColor != null)
{
if (s.gradientColor != null)
{
var id = this.getSvgGradient(String(s.fillColor), String(s.gradientColor),
s.gradientFillAlpha, s.gradientAlpha, s.gradientDirection);
if (!mxClient.IS_CHROMEAPP && !mxClient.IS_IE && !mxClient.IS_IE11 &&
!mxClient.IS_EDGE && this.root.ownerDocument == document)
{
// Workaround for potential base tag and brackets must be escaped
var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1');
this.node.setAttribute('fill', 'url(' + base + '#' + id + ')');
}
else
{
this.node.setAttribute('fill', 'url(#' + id + ')');
}
}
else
{
this.node.setAttribute('fill', String(s.fillColor).toLowerCase());
}
}
};
/**
* Function: getCurrentStrokeWidth
*
* Returns the current stroke width (>= 1), ie. max(1, this.format(this.state.strokeWidth * this.state.scale)).
*/
mxSvgCanvas2D.prototype.getCurrentStrokeWidth = function()
{
return Math.max(this.minStrokeWidth, Math.max(0.01, this.format(this.state.strokeWidth * this.state.scale)));
};
/**
* Function: updateStroke
*
* Transfers the stroke attributes from <state> to <node>.
*/
mxSvgCanvas2D.prototype.updateStroke = function()
{
var s = this.state;
this.node.setAttribute('stroke', String(s.strokeColor).toLowerCase());
if (s.alpha < 1 || s.strokeAlpha < 1)
{
this.node.setAttribute('stroke-opacity', s.alpha * s.strokeAlpha);
}
var sw = this.getCurrentStrokeWidth();
if (sw != 1)
{
this.node.setAttribute('stroke-width', sw);
}
if (this.node.nodeName == 'path')
{
this.updateStrokeAttributes();
}
if (s.dashed)
{
this.node.setAttribute('stroke-dasharray', this.createDashPattern(
((s.fixDash) ? 1 : s.strokeWidth) * s.scale));
}
};
/**
* Function: updateStrokeAttributes
*
* Transfers the stroke attributes from <state> to <node>.
*/
mxSvgCanvas2D.prototype.updateStrokeAttributes = function()
{
var s = this.state;
// Linejoin miter is default in SVG
if (s.lineJoin != null && s.lineJoin != 'miter')
{
this.node.setAttribute('stroke-linejoin', s.lineJoin);
}
if (s.lineCap != null)
{
// flat is called butt in SVG
var value = s.lineCap;
if (value == 'flat')
{
value = 'butt';
}
// Linecap butt is default in SVG
if (value != 'butt')
{
this.node.setAttribute('stroke-linecap', value);
}
}
// Miterlimit 10 is default in our document
if (s.miterLimit != null && (!this.styleEnabled || s.miterLimit != 10))
{
this.node.setAttribute('stroke-miterlimit', s.miterLimit);
}
};
/**
* Function: createDashPattern
*
* Creates the SVG dash pattern for the given state.
*/
mxSvgCanvas2D.prototype.createDashPattern = function(scale)
{
var pat = [];
if (typeof(this.state.dashPattern) === 'string')
{
var dash = this.state.dashPattern.split(' ');
if (dash.length > 0)
{
for (var i = 0; i < dash.length; i++)
{
pat[i] = Number(dash[i]) * scale;
}
}
}
return pat.join(' ');
};
/**
* Function: createTolerance
*
* Creates a hit detection tolerance shape for the given node.
*/
mxSvgCanvas2D.prototype.createTolerance = function(node)
{
var tol = node.cloneNode(true);
var sw = parseFloat(tol.getAttribute('stroke-width') || 1) + this.strokeTolerance;
tol.setAttribute('pointer-events', 'stroke');
tol.setAttribute('visibility', 'hidden');
tol.removeAttribute('stroke-dasharray');
tol.setAttribute('stroke-width', sw);
tol.setAttribute('fill', 'none');
// Workaround for Opera ignoring the visiblity attribute above while
// other browsers need a stroke color to perform the hit-detection but
// do not ignore the visibility attribute. Side-effect is that Opera's
// hit detection for horizontal/vertical edges seems to ignore the tol.
tol.setAttribute('stroke', (mxClient.IS_OT) ? 'none' : 'white');
return tol;
};
/**
* Function: createShadow
*
* Creates a shadow for the given node.
*/
mxSvgCanvas2D.prototype.createShadow = function(node)
{
var shadow = node.cloneNode(true);
var s = this.state;
// Firefox uses transparent for no fill in ellipses
if (shadow.getAttribute('fill') != 'none' && (!mxClient.IS_FF || shadow.getAttribute('fill') != 'transparent'))
{
shadow.setAttribute('fill', s.shadowColor);
}
if (shadow.getAttribute('stroke') != 'none')
{
shadow.setAttribute('stroke', s.shadowColor);
}
shadow.setAttribute('transform', 'translate(' + this.format(s.shadowDx * s.scale) +
',' + this.format(s.shadowDy * s.scale) + ')' + (s.transform || ''));
shadow.setAttribute('opacity', s.shadowAlpha);
return shadow;
};
/**
* Function: setLink
*
* Experimental implementation for hyperlinks.
*/
mxSvgCanvas2D.prototype.setLink = function(link)
{
if (link == null)
{
this.root = this.originalRoot;
}
else
{
this.originalRoot = this.root;
var node = this.createElement('a');
// Workaround for implicit namespace handling in HTML5 export, IE adds NS1 namespace so use code below
// in all IE versions except quirks mode. KNOWN: Adds xlink namespace to each image tag in output.
if (node.setAttributeNS == null || (this.root.ownerDocument != document && document.documentMode == null))
{
node.setAttribute('xlink:href', link);
}
else
{
node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', link);
}
this.root.appendChild(node);
this.root = node;
}
};
/**
* Function: rotate
*
* Sets the rotation of the canvas. Note that rotation cannot be concatenated.
*/
mxSvgCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
{
if (theta != 0 || flipH || flipV)
{
var s = this.state;
cx += s.dx;
cy += s.dy;
cx *= s.scale;
cy *= s.scale;
s.transform = s.transform || '';
// This implementation uses custom scale/translate and built-in rotation
// Rotation state is part of the AffineTransform in state.transform
if (flipH && flipV)
{
theta += 180;
}
else if (flipH != flipV)
{
var tx = (flipH) ? cx : 0;
var sx = (flipH) ? -1 : 1;
var ty = (flipV) ? cy : 0;
var sy = (flipV) ? -1 : 1;
s.transform += 'translate(' + this.format(tx) + ',' + this.format(ty) + ')' +
'scale(' + this.format(sx) + ',' + this.format(sy) + ')' +
'translate(' + this.format(-tx) + ',' + this.format(-ty) + ')';
}
if (flipH ? !flipV : flipV)
{
theta *= -1;
}
if (theta != 0)
{
s.transform += 'rotate(' + this.format(theta) + ',' + this.format(cx) + ',' + this.format(cy) + ')';
}
s.rotation = s.rotation + theta;
s.rotationCx = cx;
s.rotationCy = cy;
}
};
/**
* Function: begin
*
* Extends superclass to create path.
*/
mxSvgCanvas2D.prototype.begin = function()
{
mxAbstractCanvas2D.prototype.begin.apply(this, arguments);
this.node = this.createElement('path');
};
/**
* Function: rect
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.rect = function(x, y, w, h)
{
var s = this.state;
var n = this.createElement('rect');
n.setAttribute('x', this.format((x + s.dx) * s.scale));
n.setAttribute('y', this.format((y + s.dy) * s.scale));
n.setAttribute('width', this.format(w * s.scale));
n.setAttribute('height', this.format(h * s.scale));
this.node = n;
};
/**
* Function: roundrect
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy)
{
this.rect(x, y, w, h);
if (dx > 0)
{
this.node.setAttribute('rx', this.format(dx * this.state.scale));
}
if (dy > 0)
{
this.node.setAttribute('ry', this.format(dy * this.state.scale));
}
};
/**
* Function: ellipse
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.ellipse = function(x, y, w, h)
{
var s = this.state;
var n = this.createElement('ellipse');
// No rounding for consistent output with 1.x
n.setAttribute('cx', this.format((x + w / 2 + s.dx) * s.scale));
n.setAttribute('cy', this.format((y + h / 2 + s.dy) * s.scale));
n.setAttribute('rx', w / 2 * s.scale);
n.setAttribute('ry', h / 2 * s.scale);
this.node = n;
};
/**
* Function: image
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
{
src = this.converter.convert(src);
// LATER: Add option for embedding images as base64.
aspect = (aspect != null) ? aspect : true;
flipH = (flipH != null) ? flipH : false;
flipV = (flipV != null) ? flipV : false;
var s = this.state;
x += s.dx;
y += s.dy;
var node = this.createElement('image');
node.setAttribute('x', this.format(x * s.scale) + this.imageOffset);
node.setAttribute('y', this.format(y * s.scale) + this.imageOffset);
node.setAttribute('width', this.format(w * s.scale));
node.setAttribute('height', this.format(h * s.scale));
// Workaround for missing namespace support
if (node.setAttributeNS == null)
{
node.setAttribute('xlink:href', src);
}
else
{
node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', src);
}
if (!aspect)
{
node.setAttribute('preserveAspectRatio', 'none');
}
if (s.alpha < 1 || s.fillAlpha < 1)
{
node.setAttribute('opacity', s.alpha * s.fillAlpha);
}
var tr = this.state.transform || '';
if (flipH || flipV)
{
var sx = 1;
var sy = 1;
var dx = 0;
var dy = 0;
if (flipH)
{
sx = -1;
dx = -w - 2 * x;
}
if (flipV)
{
sy = -1;
dy = -h - 2 * y;
}
// Adds image tansformation to existing transform
tr += 'scale(' + sx + ',' + sy + ')translate(' + (dx * s.scale) + ',' + (dy * s.scale) + ')';
}
if (tr.length > 0)
{
node.setAttribute('transform', tr);
}
if (!this.pointerEvents)
{
node.setAttribute('pointer-events', 'none');
}
this.root.appendChild(node);
// Disables control-clicks on images in Firefox to open in new tab
// by putting a rect in the foreground that absorbs all events and
// disabling all pointer-events on the original image tag.
if (this.blockImagePointerEvents)
{
node.setAttribute('style', 'pointer-events:none');
node = this.createElement('rect');
node.setAttribute('visibility', 'hidden');
node.setAttribute('pointer-events', 'fill');
node.setAttribute('x', this.format(x * s.scale));
node.setAttribute('y', this.format(y * s.scale));
node.setAttribute('width', this.format(w * s.scale));
node.setAttribute('height', this.format(h * s.scale));
this.root.appendChild(node);
}
};
/**
* Function: convertHtml
*
* Converts the given HTML string to XHTML.
*/
mxSvgCanvas2D.prototype.convertHtml = function(val)
{
if (this.useDomParser)
{
var doc = new DOMParser().parseFromString(val, 'text/html');
if (doc != null)
{
val = new XMLSerializer().serializeToString(doc.body);
// Extracts body content from DOM
if (val.substring(0, 5) == '<body')
{
val = val.substring(val.indexOf('>', 5) + 1);
}
if (val.substring(val.length - 7, val.length) == '</body>')
{
val = val.substring(0, val.length - 7);
}
}
}
else if (document.implementation != null && document.implementation.createDocument != null)
{
var xd = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null);
var xb = xd.createElement('body');
xd.documentElement.appendChild(xb);
var div = document.createElement('div');
div.innerHTML = val;
var child = div.firstChild;
while (child != null)
{
var next = child.nextSibling;
xb.appendChild(xd.adoptNode(child));
child = next;
}
return xb.innerHTML;
}
else
{
var ta = document.createElement('textarea');
// Handles special HTML entities < and > and double escaping
// and converts unclosed br, hr and img tags to XHTML
// LATER: Convert all unclosed tags
ta.innerHTML = val.replace(/&amp;/g, '&amp;amp;').
replace(/&#60;/g, '&amp;lt;').replace(/&#62;/g, '&amp;gt;').
replace(/&lt;/g, '&amp;lt;').replace(/&gt;/g, '&amp;gt;').
replace(/</g, '&lt;').replace(/>/g, '&gt;');
val = ta.value.replace(/&/g, '&amp;').replace(/&amp;lt;/g, '&lt;').
replace(/&amp;gt;/g, '&gt;').replace(/&amp;amp;/g, '&amp;').
replace(/<br>/g, '<br />').replace(/<hr>/g, '<hr />').
replace(/(<img[^>]+)>/gm, "$1 />");
}
return val;
};
/**
* Function: createDiv
*
* Private helper function to create SVG elements
*/
mxSvgCanvas2D.prototype.createDiv = function(str, align, valign, style, overflow, whiteSpace)
{
var s = this.state;
// Inline block for rendering HTML background over SVG in Safari
var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' :
(mxConstants.LINE_HEIGHT * this.lineHeightCorrection);
style = 'display:inline-block;font-size:' + s.fontSize + 'px;font-family:' + s.fontFamily +
';color:' + s.fontColor + ';line-height:' + lh + ';' + style;
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
style += 'font-weight:bold;';
}
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
style += 'font-style:italic;';
}
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
style += 'text-decoration:underline;';
}
if (align == mxConstants.ALIGN_CENTER)
{
style += 'text-align:center;';
}
else if (align == mxConstants.ALIGN_RIGHT)
{
style += 'text-align:right;';
}
else
{
style += 'text-align:left;';
}
var css = '';
if (s.fontBackgroundColor != null)
{
css += 'background-color:' + mxUtils.htmlEntities(s.fontBackgroundColor) + ';';
}
if (s.fontBorderColor != null)
{
css += 'border:1px solid ' + mxUtils.htmlEntities(s.fontBorderColor) + ';';
}
var val = str;
if (!mxUtils.isNode(val))
{
val = this.convertHtml(val);
if (overflow != 'fill' && overflow != 'width')
{
// Workaround for no wrapping in HTML canvas for image
// export if the inner HTML contains a DIV with width
if (whiteSpace != null)
{
css += 'white-space:' + whiteSpace + ';';
}
// Inner div always needed to measure wrapped text
val = '<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;' + css + '">' + val + '</div>';
}
else
{
style += css;
}
}
// Uses DOM API where available. This cannot be used in IE to avoid
// an opening and two (!) closing TBODY tags being added to tables.
if (!mxClient.IS_IE && document.createElementNS)
{
var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
div.setAttribute('style', style);
if (mxUtils.isNode(val))
{
// Creates a copy for export
if (this.root.ownerDocument != document)
{
div.appendChild(val.cloneNode(true));
}
else
{
div.appendChild(val);
}
}
else
{
div.innerHTML = val;
}
return div;
}
else
{
// Serializes for export
if (mxUtils.isNode(val) && this.root.ownerDocument != document)
{
val = val.outerHTML;
}
// NOTE: FF 3.6 crashes if content CSS contains "height:100%"
return mxUtils.parseXml('<div xmlns="http://www.w3.org/1999/xhtml" style="' + style +
'">' + val + '</div>').documentElement;
}
};
/**
* Invalidates the cached offset size for the given node.
*/
mxSvgCanvas2D.prototype.invalidateCachedOffsetSize = function(node)
{
delete node.firstChild.mxCachedOffsetWidth;
delete node.firstChild.mxCachedFinalOffsetWidth;
delete node.firstChild.mxCachedFinalOffsetHeight;
};
/**
* Updates existing DOM nodes for text rendering. LATER: Merge common parts with text function below.
*/
mxSvgCanvas2D.prototype.updateText = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, node)
{
if (node != null && node.firstChild != null && node.firstChild.firstChild != null &&
node.firstChild.firstChild.firstChild != null)
{
// Uses outer group for opacity and transforms to
// fix rendering order in Chrome
var group = node.firstChild;
var fo = group.firstChild;
var div = fo.firstChild;
rotation = (rotation != null) ? rotation : 0;
var s = this.state;
x += s.dx;
y += s.dy;
if (clip)
{
div.style.maxHeight = Math.round(h) + 'px';
div.style.maxWidth = Math.round(w) + 'px';
}
else if (overflow == 'fill')
{
div.style.width = Math.round(w + 1) + 'px';
div.style.height = Math.round(h + 1) + 'px';
}
else if (overflow == 'width')
{
div.style.width = Math.round(w + 1) + 'px';
if (h > 0)
{
div.style.maxHeight = Math.round(h) + 'px';
}
}
if (wrap && w > 0)
{
div.style.width = Math.round(w + 1) + 'px';
}
// Code that depends on the size which is computed after
// the element was added to the DOM.
var ow = 0;
var oh = 0;
// Padding avoids clipping on border and wrapping for differing font metrics on platforms
var padX = 0;
var padY = 2;
var sizeDiv = div;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
}
var tmp = (group.mxCachedOffsetWidth != null) ? group.mxCachedOffsetWidth : sizeDiv.offsetWidth;
ow = tmp + padX;
// Recomputes the height of the element for wrapped width
if (wrap && overflow != 'fill')
{
if (clip)
{
ow = Math.min(ow, w);
}
div.style.width = Math.round(ow + 1) + 'px';
}
ow = (group.mxCachedFinalOffsetWidth != null) ? group.mxCachedFinalOffsetWidth : sizeDiv.offsetWidth;
oh = (group.mxCachedFinalOffsetHeight != null) ? group.mxCachedFinalOffsetHeight : sizeDiv.offsetHeight;
if (this.cacheOffsetSize)
{
group.mxCachedOffsetWidth = tmp;
group.mxCachedFinalOffsetWidth = ow;
group.mxCachedFinalOffsetHeight = oh;
}
ow += padX;
oh -= 2;
if (clip)
{
oh = Math.min(oh, h);
ow = Math.min(ow, w);
}
if (overflow == 'width')
{
h = oh;
}
else if (overflow != 'fill')
{
w = ow;
h = oh;
}
var dx = 0;
var dy = 0;
if (align == mxConstants.ALIGN_CENTER)
{
dx -= w / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
dx -= w;
}
x += dx;
// FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export
if (valign == mxConstants.ALIGN_MIDDLE)
{
dy -= h / 2;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
dy -= h;
}
// Workaround for rendering offsets
// TODO: Check if export needs these fixes, too
if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN)
{
dy -= 2;
}
y += dy;
var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : '';
if (s.rotation != 0 && this.rotateHtml)
{
tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')';
var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale,
s.rotation, s.rotationCx, s.rotationCy);
x = pt.x - w * s.scale / 2;
y = pt.y - h * s.scale / 2;
}
else
{
x *= s.scale;
y *= s.scale;
}
if (rotation != 0)
{
tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')';
}
group.setAttribute('transform', 'translate(' + Math.round(x) + ',' + Math.round(y) + ')' + tr);
fo.setAttribute('width', Math.round(Math.max(1, w)));
fo.setAttribute('height', Math.round(Math.max(1, h)));
}
};
/**
* Function: text
*
* Paints the given text. Possible values for format are empty string for plain
* text and html for HTML markup. Note that HTML markup is only supported if
* foreignObject is supported and <foEnabled> is true. (This means IE9 and later
* does currently not support HTML text as part of shapes.)
*/
mxSvgCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)
{
if (this.textEnabled && str != null)
{
rotation = (rotation != null) ? rotation : 0;
var s = this.state;
x += s.dx;
y += s.dy;
if (this.foEnabled && format == 'html')
{
var style = 'vertical-align:top;';
if (clip)
{
style += 'overflow:hidden;max-height:' + Math.round(h) + 'px;max-width:' + Math.round(w) + 'px;';
}
else if (overflow == 'fill')
{
style += 'width:' + Math.round(w + 1) + 'px;height:' + Math.round(h + 1) + 'px;overflow:hidden;';
}
else if (overflow == 'width')
{
style += 'width:' + Math.round(w + 1) + 'px;';
if (h > 0)
{
style += 'max-height:' + Math.round(h) + 'px;overflow:hidden;';
}
}
if (wrap && w > 0)
{
style += 'width:' + Math.round(w + 1) + 'px;white-space:normal;word-wrap:' +
mxConstants.WORD_WRAP + ';';
}
else
{
style += 'white-space:nowrap;';
}
// Uses outer group for opacity and transforms to
// fix rendering order in Chrome
var group = this.createElement('g');
if (s.alpha < 1)
{
group.setAttribute('opacity', s.alpha);
}
var fo = this.createElement('foreignObject');
fo.setAttribute('style', 'overflow:visible;');
fo.setAttribute('pointer-events', (this.pointerEvents) ? this.pointerEventsValue : 'none');
var div = this.createDiv(str, align, valign, style, overflow, (wrap && w > 0) ? 'normal' : null);
// Ignores invalid XHTML labels
if (div == null)
{
return;
}
else if (dir != null)
{
div.setAttribute('dir', dir);
}
group.appendChild(fo);
this.root.appendChild(group);
// Code that depends on the size which is computed after
// the element was added to the DOM.
var ow = 0;
var oh = 0;
// Padding avoids clipping on border and wrapping for differing font metrics on platforms
var padX = 2;
var padY = 2;
// NOTE: IE is always export as it does not support foreign objects
if (mxClient.IS_IE && (document.documentMode == 9 || !mxClient.IS_SVG))
{
// Handles non-standard namespace for getting size in IE
var clone = document.createElement('div');
clone.style.cssText = div.getAttribute('style');
clone.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
// Inner DIV is needed for text measuring
var div2 = document.createElement('div');
div2.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
div2.style.wordWrap = mxConstants.WORD_WRAP;
div2.innerHTML = (mxUtils.isNode(str)) ? str.outerHTML : str;
clone.appendChild(div2);
document.body.appendChild(clone);
// Workaround for different box models
if (document.documentMode != 8 && document.documentMode != 9 && s.fontBorderColor != null)
{
padX += 2;
padY += 2;
}
if (wrap && w > 0)
{
var tmp = div2.offsetWidth;
// Workaround for adding padding twice in IE8/IE9 standards mode if label is wrapped
padDx = 0;
// For export, if no wrapping occurs, we add a large padding to make
// sure there is no wrapping even if the text metrics are different.
// This adds support for text metrics on different operating systems.
// Disables wrapping if text is not wrapped for given width
if (!clip && wrap && w > 0 && this.root.ownerDocument != document && overflow != 'fill')
{
var ws = clone.style.whiteSpace;
div2.style.whiteSpace = 'nowrap';
if (tmp < div2.offsetWidth)
{
clone.style.whiteSpace = ws;
}
}
if (clip)
{
tmp = Math.min(tmp, w);
}
clone.style.width = tmp + 'px';
// Padding avoids clipping on border
ow = div2.offsetWidth + padX + padDx;
oh = div2.offsetHeight + padY;
// Overrides the width of the DIV via XML DOM by using the
// clone DOM style, getting the CSS text for that and
// then setting that on the DIV via setAttribute
clone.style.display = 'inline-block';
clone.style.position = '';
clone.style.visibility = '';
clone.style.width = ow + 'px';
div.setAttribute('style', clone.style.cssText);
}
else
{
// Padding avoids clipping on border
ow = div2.offsetWidth + padX;
oh = div2.offsetHeight + padY;
}
clone.parentNode.removeChild(clone);
fo.appendChild(div);
}
else
{
// Uses document for text measuring during export
if (this.root.ownerDocument != document)
{
div.style.visibility = 'hidden';
document.body.appendChild(div);
}
else
{
fo.appendChild(div);
}
var sizeDiv = div;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
if (wrap && div.style.wordWrap == 'break-word')
{
sizeDiv.style.width = '100%';
}
}
var tmp = sizeDiv.offsetWidth;
// Workaround for text measuring in hidden containers
if (tmp == 0 && div.parentNode == fo)
{
div.style.visibility = 'hidden';
document.body.appendChild(div);
tmp = sizeDiv.offsetWidth;
}
if (this.cacheOffsetSize)
{
group.mxCachedOffsetWidth = tmp;
}
// Disables wrapping if text is not wrapped for given width
if (!clip && wrap && w > 0 && this.root.ownerDocument != document &&
overflow != 'fill' && overflow != 'width')
{
var ws = div.style.whiteSpace;
div.style.whiteSpace = 'nowrap';
if (tmp < sizeDiv.offsetWidth)
{
div.style.whiteSpace = ws;
}
}
ow = tmp + padX - 1;
// Recomputes the height of the element for wrapped width
if (wrap && overflow != 'fill' && overflow != 'width')
{
if (clip)
{
ow = Math.min(ow, w);
}
div.style.width = ow + 'px';
}
ow = sizeDiv.offsetWidth;
oh = sizeDiv.offsetHeight;
if (this.cacheOffsetSize)
{
group.mxCachedFinalOffsetWidth = ow;
group.mxCachedFinalOffsetHeight = oh;
}
oh -= padY;
if (div.parentNode != fo)
{
fo.appendChild(div);
div.style.visibility = '';
}
}
if (clip)
{
oh = Math.min(oh, h);
ow = Math.min(ow, w);
}
if (overflow == 'width')
{
h = oh;
}
else if (overflow != 'fill')
{
w = ow;
h = oh;
}
if (s.alpha < 1)
{
group.setAttribute('opacity', s.alpha);
}
var dx = 0;
var dy = 0;
if (align == mxConstants.ALIGN_CENTER)
{
dx -= w / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
dx -= w;
}
x += dx;
// FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export
if (valign == mxConstants.ALIGN_MIDDLE)
{
dy -= h / 2;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
dy -= h;
}
// Workaround for rendering offsets
// TODO: Check if export needs these fixes, too
//if (this.root.ownerDocument == document)
if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN)
{
dy -= 2;
}
y += dy;
var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : '';
if (s.rotation != 0 && this.rotateHtml)
{
tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')';
var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale,
s.rotation, s.rotationCx, s.rotationCy);
x = pt.x - w * s.scale / 2;
y = pt.y - h * s.scale / 2;
}
else
{
x *= s.scale;
y *= s.scale;
}
if (rotation != 0)
{
tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')';
}
group.setAttribute('transform', 'translate(' + (Math.round(x) + this.foOffset) + ',' +
(Math.round(y) + this.foOffset) + ')' + tr);
fo.setAttribute('width', Math.round(Math.max(1, w)));
fo.setAttribute('height', Math.round(Math.max(1, h)));
// Adds alternate content if foreignObject not supported in viewer
if (this.root.ownerDocument != document)
{
var alt = this.createAlternateContent(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation);
if (alt != null)
{
fo.setAttribute('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility');
var sw = this.createElement('switch');
sw.appendChild(fo);
sw.appendChild(alt);
group.appendChild(sw);
}
}
}
else
{
this.plainText(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir);
}
}
};
/**
* Function: createClip
*
* Creates a clip for the given coordinates.
*/
mxSvgCanvas2D.prototype.createClip = function(x, y, w, h)
{
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
var id = 'mx-clip-' + x + '-' + y + '-' + w + '-' + h;
var counter = 0;
var tmp = id + '-' + counter;
// Resolves ID conflicts
while (document.getElementById(tmp) != null)
{
tmp = id + '-' + (++counter);
}
clip = this.createElement('clipPath');
clip.setAttribute('id', tmp);
var rect = this.createElement('rect');
rect.setAttribute('x', x);
rect.setAttribute('y', y);
rect.setAttribute('width', w);
rect.setAttribute('height', h);
clip.appendChild(rect);
return clip;
};
/**
* Function: text
*
* Paints the given text. Possible values for format are empty string for
* plain text and html for HTML markup.
*/
mxSvgCanvas2D.prototype.plainText = function(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir)
{
rotation = (rotation != null) ? rotation : 0;
var s = this.state;
var size = s.fontSize;
var node = this.createElement('g');
var tr = s.transform || '';
this.updateFont(node);
// Non-rotated text
if (rotation != 0)
{
tr += 'rotate(' + rotation + ',' + this.format(x * s.scale) + ',' + this.format(y * s.scale) + ')';
}
if (dir != null)
{
node.setAttribute('direction', dir);
}
if (clip && w > 0 && h > 0)
{
var cx = x;
var cy = y;
if (align == mxConstants.ALIGN_CENTER)
{
cx -= w / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
cx -= w;
}
if (overflow != 'fill')
{
if (valign == mxConstants.ALIGN_MIDDLE)
{
cy -= h / 2;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
cy -= h;
}
}
// LATER: Remove spacing from clip rectangle
var c = this.createClip(cx * s.scale - 2, cy * s.scale - 2, w * s.scale + 4, h * s.scale + 4);
if (this.defs != null)
{
this.defs.appendChild(c);
}
else
{
// Makes sure clip is removed with referencing node
this.root.appendChild(c);
}
if (!mxClient.IS_CHROMEAPP && !mxClient.IS_IE && !mxClient.IS_IE11 &&
!mxClient.IS_EDGE && this.root.ownerDocument == document)
{
// Workaround for potential base tag
var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1');
node.setAttribute('clip-path', 'url(' + base + '#' + c.getAttribute('id') + ')');
}
else
{
node.setAttribute('clip-path', 'url(#' + c.getAttribute('id') + ')');
}
}
// Default is left
var anchor = (align == mxConstants.ALIGN_RIGHT) ? 'end' :
(align == mxConstants.ALIGN_CENTER) ? 'middle' :
'start';
// Text-anchor start is default in SVG
if (anchor != 'start')
{
node.setAttribute('text-anchor', anchor);
}
if (!this.styleEnabled || size != mxConstants.DEFAULT_FONTSIZE)
{
node.setAttribute('font-size', (size * s.scale) + 'px');
}
if (tr.length > 0)
{
node.setAttribute('transform', tr);
}
if (s.alpha < 1)
{
node.setAttribute('opacity', s.alpha);
}
var lines = str.split('\n');
var lh = Math.round(size * mxConstants.LINE_HEIGHT);
var textHeight = size + (lines.length - 1) * lh;
var cy = y + size - 1;
if (valign == mxConstants.ALIGN_MIDDLE)
{
if (overflow == 'fill')
{
cy -= h / 2;
}
else
{
var dy = ((this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight) / 2;
cy -= dy + 1;
}
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
if (overflow == 'fill')
{
cy -= h;
}
else
{
var dy = (this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight;
cy -= dy + 2;
}
}
for (var i = 0; i < lines.length; i++)
{
// Workaround for bounding box of empty lines and spaces
if (lines[i].length > 0 && mxUtils.trim(lines[i]).length > 0)
{
var text = this.createElement('text');
// LATER: Match horizontal HTML alignment
text.setAttribute('x', this.format(x * s.scale) + this.textOffset);
text.setAttribute('y', this.format(cy * s.scale) + this.textOffset);
mxUtils.write(text, lines[i]);
node.appendChild(text);
}
cy += lh;
}
this.root.appendChild(node);
this.addTextBackground(node, str, x, y, w, (overflow == 'fill') ? h : textHeight, align, valign, overflow);
};
/**
* Function: updateFont
*
* Updates the text properties for the given node. (NOTE: For this to work in
* IE, the given node must be a text or tspan element.)
*/
mxSvgCanvas2D.prototype.updateFont = function(node)
{
var s = this.state;
node.setAttribute('fill', s.fontColor);
if (!this.styleEnabled || s.fontFamily != mxConstants.DEFAULT_FONTFAMILY)
{
node.setAttribute('font-family', s.fontFamily);
}
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
node.setAttribute('font-weight', 'bold');
}
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
node.setAttribute('font-style', 'italic');
}
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
node.setAttribute('text-decoration', 'underline');
}
};
/**
* Function: addTextBackground
*
* Background color and border
*/
mxSvgCanvas2D.prototype.addTextBackground = function(node, str, x, y, w, h, align, valign, overflow)
{
var s = this.state;
if (s.fontBackgroundColor != null || s.fontBorderColor != null)
{
var bbox = null;
if (overflow == 'fill' || overflow == 'width')
{
if (align == mxConstants.ALIGN_CENTER)
{
x -= w / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
x -= w;
}
if (valign == mxConstants.ALIGN_MIDDLE)
{
y -= h / 2;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
y -= h;
}
bbox = new mxRectangle((x + 1) * s.scale, y * s.scale, (w - 2) * s.scale, (h + 2) * s.scale);
}
else if (node.getBBox != null && this.root.ownerDocument == document)
{
// Uses getBBox only if inside document for correct size
try
{
bbox = node.getBBox();
var ie = mxClient.IS_IE && mxClient.IS_SVG;
bbox = new mxRectangle(bbox.x, bbox.y + ((ie) ? 0 : 1), bbox.width, bbox.height + ((ie) ? 1 : 0));
}
catch (e)
{
// Ignores NS_ERROR_FAILURE in FF if container display is none.
}
}
else
{
// Computes size if not in document or no getBBox available
var div = document.createElement('div');
// Wrapping and clipping can be ignored here
div.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT;
div.style.fontSize = s.fontSize + 'px';
div.style.fontFamily = s.fontFamily;
div.style.whiteSpace = 'nowrap';
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
div.style.zoom = '1';
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
div.style.fontWeight = 'bold';
}
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
div.style.fontStyle = 'italic';
}
str = mxUtils.htmlEntities(str, false);
div.innerHTML = str.replace(/\n/g, '<br/>');
document.body.appendChild(div);
var w = div.offsetWidth;
var h = div.offsetHeight;
div.parentNode.removeChild(div);
if (align == mxConstants.ALIGN_CENTER)
{
x -= w / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
x -= w;
}
if (valign == mxConstants.ALIGN_MIDDLE)
{
y -= h / 2;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
y -= h;
}
bbox = new mxRectangle((x + 1) * s.scale, (y + 2) * s.scale, w * s.scale, (h + 1) * s.scale);
}
if (bbox != null)
{
var n = this.createElement('rect');
n.setAttribute('fill', s.fontBackgroundColor || 'none');
n.setAttribute('stroke', s.fontBorderColor || 'none');
n.setAttribute('x', Math.floor(bbox.x - 1));
n.setAttribute('y', Math.floor(bbox.y - 1));
n.setAttribute('width', Math.ceil(bbox.width + 2));
n.setAttribute('height', Math.ceil(bbox.height));
var sw = (s.fontBorderColor != null) ? Math.max(1, this.format(s.scale)) : 0;
n.setAttribute('stroke-width', sw);
// Workaround for crisp rendering - only required if not exporting
if (this.root.ownerDocument == document && mxUtils.mod(sw, 2) == 1)
{
n.setAttribute('transform', 'translate(0.5, 0.5)');
}
node.insertBefore(n, node.firstChild);
}
}
};
/**
* Function: stroke
*
* Paints the outline of the current path.
*/
mxSvgCanvas2D.prototype.stroke = function()
{
this.addNode(false, true);
};
/**
* Function: fill
*
* Fills the current path.
*/
mxSvgCanvas2D.prototype.fill = function()
{
this.addNode(true, false);
};
/**
* Function: fillAndStroke
*
* Fills and paints the outline of the current path.
*/
mxSvgCanvas2D.prototype.fillAndStroke = function()
{
this.addNode(true, true);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
*
* Class: mxVmlCanvas2D
*
* Implements a canvas to be used for rendering VML. Here is an example of implementing a
* fallback for SVG images which are not supported in VML-based browsers.
*
* (code)
* var mxVmlCanvas2DImage = mxVmlCanvas2D.prototype.image;
* mxVmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
* {
* if (src.substring(src.length - 4, src.length) == '.svg')
* {
* src = 'http://www.jgraph.com/images/mxgraph.gif';
* }
*
* mxVmlCanvas2DImage.apply(this, arguments);
* };
* (end)
*
* To disable anti-aliasing in the output, use the following code.
*
* (code)
* document.createStyleSheet().cssText = mxClient.VML_PREFIX + '\\:*{antialias:false;)}';
* (end)
*
* A description of the public API is available in <mxXmlCanvas2D>. Note that
* there is a known issue in VML where gradients are painted using the outer
* bounding box of rotated shapes, not the actual bounds of the shape. See
* also <text> for plain text label restrictions in shapes for VML.
*/
var mxVmlCanvas2D = function(root)
{
mxAbstractCanvas2D.call(this);
/**
* Variable: root
*
* Reference to the container for the SVG content.
*/
this.root = root;
};
/**
* Extends mxAbstractCanvas2D
*/
mxUtils.extend(mxVmlCanvas2D, mxAbstractCanvas2D);
/**
* Variable: path
*
* Holds the current DOM node.
*/
mxVmlCanvas2D.prototype.node = null;
/**
* Variable: textEnabled
*
* Specifies if text output should be enabledetB. Default is true.
*/
mxVmlCanvas2D.prototype.textEnabled = true;
/**
* Variable: moveOp
*
* Contains the string used for moving in paths. Default is 'm'.
*/
mxVmlCanvas2D.prototype.moveOp = 'm';
/**
* Variable: lineOp
*
* Contains the string used for moving in paths. Default is 'l'.
*/
mxVmlCanvas2D.prototype.lineOp = 'l';
/**
* Variable: curveOp
*
* Contains the string used for bezier curves. Default is 'c'.
*/
mxVmlCanvas2D.prototype.curveOp = 'c';
/**
* Variable: closeOp
*
* Holds the operator for closing curves. Default is 'x e'.
*/
mxVmlCanvas2D.prototype.closeOp = 'x';
/**
* Variable: rotatedHtmlBackground
*
* Background color for rotated HTML. Default is ''. This can be set to eg.
* white to improve rendering of rotated text in VML for IE9.
*/
mxVmlCanvas2D.prototype.rotatedHtmlBackground = '';
/**
* Variable: vmlScale
*
* Specifies the scale used to draw VML shapes.
*/
mxVmlCanvas2D.prototype.vmlScale = 1;
/**
* Function: createElement
*
* Creates the given element using the document.
*/
mxVmlCanvas2D.prototype.createElement = function(name)
{
return document.createElement(name);
};
/**
* Function: createVmlElement
*
* Creates a new element using <createElement> and prefixes the given name with
* <mxClient.VML_PREFIX>.
*/
mxVmlCanvas2D.prototype.createVmlElement = function(name)
{
return this.createElement(mxClient.VML_PREFIX + ':' + name);
};
/**
* Function: addNode
*
* Adds the current node to the <root>.
*/
mxVmlCanvas2D.prototype.addNode = function(filled, stroked)
{
var node = this.node;
var s = this.state;
if (node != null)
{
if (node.nodeName == 'shape')
{
// Checks if the path is not empty
if (this.path != null && this.path.length > 0)
{
node.path = this.path.join(' ') + ' e';
node.style.width = this.root.style.width;
node.style.height = this.root.style.height;
node.coordsize = parseInt(node.style.width) + ' ' + parseInt(node.style.height);
}
else
{
return;
}
}
node.strokeweight = this.format(Math.max(1, s.strokeWidth * s.scale / this.vmlScale)) + 'px';
if (s.shadow)
{
this.root.appendChild(this.createShadow(node,
filled && s.fillColor != null,
stroked && s.strokeColor != null));
}
if (stroked && s.strokeColor != null)
{
node.stroked = 'true';
node.strokecolor = s.strokeColor;
}
else
{
node.stroked = 'false';
}
node.appendChild(this.createStroke());
if (filled && s.fillColor != null)
{
node.appendChild(this.createFill());
}
else if (this.pointerEvents && (node.nodeName != 'shape' ||
this.path[this.path.length - 1] == this.closeOp))
{
node.appendChild(this.createTransparentFill());
}
else
{
node.filled = 'false';
}
// LATER: Update existing DOM for performance
this.root.appendChild(node);
}
};
/**
* Function: createTransparentFill
*
* Creates a transparent fill.
*/
mxVmlCanvas2D.prototype.createTransparentFill = function()
{
var fill = this.createVmlElement('fill');
fill.src = mxClient.imageBasePath + '/transparent.gif';
fill.type = 'tile';
return fill;
};
/**
* Function: createFill
*
* Creates a fill for the current state.
*/
mxVmlCanvas2D.prototype.createFill = function()
{
var s = this.state;
// Gradients in foregrounds not supported because special gradients
// with bounds must be created for each element in graphics-canvases
var fill = this.createVmlElement('fill');
fill.color = s.fillColor;
if (s.gradientColor != null)
{
fill.type = 'gradient';
fill.method = 'none';
fill.color2 = s.gradientColor;
var angle = 180 - s.rotation;
if (s.gradientDirection == mxConstants.DIRECTION_WEST)
{
angle -= 90 + ((this.root.style.flip == 'x') ? 180 : 0);
}
else if (s.gradientDirection == mxConstants.DIRECTION_EAST)
{
angle += 90 + ((this.root.style.flip == 'x') ? 180 : 0);
}
else if (s.gradientDirection == mxConstants.DIRECTION_NORTH)
{
angle -= 180 + ((this.root.style.flip == 'y') ? -180 : 0);
}
else
{
angle += ((this.root.style.flip == 'y') ? -180 : 0);
}
if (this.root.style.flip == 'x' || this.root.style.flip == 'y')
{
angle *= -1;
}
// LATER: Fix outer bounding box for rotated shapes used in VML.
fill.angle = mxUtils.mod(angle, 360);
fill.opacity = (s.alpha * s.gradientFillAlpha * 100) + '%';
fill.setAttribute(mxClient.OFFICE_PREFIX + ':opacity2', (s.alpha * s.gradientAlpha * 100) + '%');
}
else if (s.alpha < 1 || s.fillAlpha < 1)
{
fill.opacity = (s.alpha * s.fillAlpha * 100) + '%';
}
return fill;
};
/**
* Function: createStroke
*
* Creates a fill for the current state.
*/
mxVmlCanvas2D.prototype.createStroke = function()
{
var s = this.state;
var stroke = this.createVmlElement('stroke');
stroke.endcap = s.lineCap || 'flat';
stroke.joinstyle = s.lineJoin || 'miter';
stroke.miterlimit = s.miterLimit || '10';
if (s.alpha < 1 || s.strokeAlpha < 1)
{
stroke.opacity = (s.alpha * s.strokeAlpha * 100) + '%';
}
if (s.dashed)
{
stroke.dashstyle = this.getVmlDashStyle();
}
return stroke;
};
/**
* Function: getVmlDashPattern
*
* Returns a VML dash pattern for the current dashPattern.
* See http://msdn.microsoft.com/en-us/library/bb264085(v=vs.85).aspx
*/
mxVmlCanvas2D.prototype.getVmlDashStyle = function()
{
var result = 'dash';
if (typeof(this.state.dashPattern) === 'string')
{
var tok = this.state.dashPattern.split(' ');
if (tok.length > 0 && tok[0] == 1)
{
result = '0 2';
}
}
return result;
};
/**
* Function: createShadow
*
* Creates a shadow for the given node.
*/
mxVmlCanvas2D.prototype.createShadow = function(node, filled, stroked)
{
var s = this.state;
var rad = -s.rotation * (Math.PI / 180);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var dx = s.shadowDx * s.scale;
var dy = s.shadowDy * s.scale;
if (this.root.style.flip == 'x')
{
dx *= -1;
}
else if (this.root.style.flip == 'y')
{
dy *= -1;
}
var shadow = node.cloneNode(true);
shadow.style.marginLeft = Math.round(dx * cos - dy * sin) + 'px';
shadow.style.marginTop = Math.round(dx * sin + dy * cos) + 'px';
// Workaround for wrong cloning in IE8 standards mode
if (document.documentMode == 8)
{
shadow.strokeweight = node.strokeweight;
if (node.nodeName == 'shape')
{
shadow.path = this.path.join(' ') + ' e';
shadow.style.width = this.root.style.width;
shadow.style.height = this.root.style.height;
shadow.coordsize = parseInt(node.style.width) + ' ' + parseInt(node.style.height);
}
}
if (stroked)
{
shadow.strokecolor = s.shadowColor;
shadow.appendChild(this.createShadowStroke());
}
else
{
shadow.stroked = 'false';
}
if (filled)
{
shadow.appendChild(this.createShadowFill());
}
else
{
shadow.filled = 'false';
}
return shadow;
};
/**
* Function: createShadowFill
*
* Creates the fill for the shadow.
*/
mxVmlCanvas2D.prototype.createShadowFill = function()
{
var fill = this.createVmlElement('fill');
fill.color = this.state.shadowColor;
fill.opacity = (this.state.alpha * this.state.shadowAlpha * 100) + '%';
return fill;
};
/**
* Function: createShadowStroke
*
* Creates the stroke for the shadow.
*/
mxVmlCanvas2D.prototype.createShadowStroke = function()
{
var stroke = this.createStroke();
stroke.opacity = (this.state.alpha * this.state.shadowAlpha * 100) + '%';
return stroke;
};
/**
* Function: rotate
*
* Sets the rotation of the canvas. Note that rotation cannot be concatenated.
*/
mxVmlCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
{
if (flipH && flipV)
{
theta += 180;
}
else if (flipH)
{
this.root.style.flip = 'x';
}
else if (flipV)
{
this.root.style.flip = 'y';
}
if (flipH ? !flipV : flipV)
{
theta *= -1;
}
this.root.style.rotation = theta;
this.state.rotation = this.state.rotation + theta;
this.state.rotationCx = cx;
this.state.rotationCy = cy;
};
/**
* Function: begin
*
* Extends superclass to create path.
*/
mxVmlCanvas2D.prototype.begin = function()
{
mxAbstractCanvas2D.prototype.begin.apply(this, arguments);
this.node = this.createVmlElement('shape');
this.node.style.position = 'absolute';
};
/**
* Function: quadTo
*
* Replaces quadratic curve with bezier curve in VML.
*/
mxVmlCanvas2D.prototype.quadTo = function(x1, y1, x2, y2)
{
var s = this.state;
var cpx0 = (this.lastX + s.dx) * s.scale;
var cpy0 = (this.lastY + s.dy) * s.scale;
var qpx1 = (x1 + s.dx) * s.scale;
var qpy1 = (y1 + s.dy) * s.scale;
var cpx3 = (x2 + s.dx) * s.scale;
var cpy3 = (y2 + s.dy) * s.scale;
var cpx1 = cpx0 + 2/3 * (qpx1 - cpx0);
var cpy1 = cpy0 + 2/3 * (qpy1 - cpy0);
var cpx2 = cpx3 + 2/3 * (qpx1 - cpx3);
var cpy2 = cpy3 + 2/3 * (qpy1 - cpy3);
this.path.push('c ' + this.format(cpx1) + ' ' + this.format(cpy1) +
' ' + this.format(cpx2) + ' ' + this.format(cpy2) +
' ' + this.format(cpx3) + ' ' + this.format(cpy3));
this.lastX = (cpx3 / s.scale) - s.dx;
this.lastY = (cpy3 / s.scale) - s.dy;
};
/**
* Function: createRect
*
* Sets the glass gradient.
*/
mxVmlCanvas2D.prototype.createRect = function(nodeName, x, y, w, h)
{
var s = this.state;
var n = this.createVmlElement(nodeName);
n.style.position = 'absolute';
n.style.left = this.format((x + s.dx) * s.scale) + 'px';
n.style.top = this.format((y + s.dy) * s.scale) + 'px';
n.style.width = this.format(w * s.scale) + 'px';
n.style.height = this.format(h * s.scale) + 'px';
return n;
};
/**
* Function: rect
*
* Sets the current path to a rectangle.
*/
mxVmlCanvas2D.prototype.rect = function(x, y, w, h)
{
this.node = this.createRect('rect', x, y, w, h);
};
/**
* Function: roundrect
*
* Sets the current path to a rounded rectangle.
*/
mxVmlCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy)
{
this.node = this.createRect('roundrect', x, y, w, h);
// SetAttribute needed here for IE8
this.node.setAttribute('arcsize', Math.max(dx * 100 / w, dy * 100 / h) + '%');
};
/**
* Function: ellipse
*
* Sets the current path to an ellipse.
*/
mxVmlCanvas2D.prototype.ellipse = function(x, y, w, h)
{
this.node = this.createRect('oval', x, y, w, h);
};
/**
* Function: image
*
* Paints an image.
*/
mxVmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
{
var node = null;
if (!aspect)
{
node = this.createRect('image', x, y, w, h);
node.src = src;
}
else
{
// Uses fill with aspect to avoid asynchronous update of size
node = this.createRect('rect', x, y, w, h);
node.stroked = 'false';
// Handles image aspect via fill
var fill = this.createVmlElement('fill');
fill.aspect = (aspect) ? 'atmost' : 'ignore';
fill.rotate = 'true';
fill.type = 'frame';
fill.src = src;
node.appendChild(fill);
}
if (flipH && flipV)
{
node.style.rotation = '180';
}
else if (flipH)
{
node.style.flip = 'x';
}
else if (flipV)
{
node.style.flip = 'y';
}
if (this.state.alpha < 1 || this.state.fillAlpha < 1)
{
// KNOWN: Borders around transparent images in IE<9. Using fill.opacity
// fixes this problem by adding a white background in all IE versions.
node.style.filter += 'alpha(opacity=' + (this.state.alpha * this.state.fillAlpha * 100) + ')';
}
this.root.appendChild(node);
};
/**
* Function: createText
*
* Creates the innermost element that contains the HTML text.
*/
mxVmlCanvas2D.prototype.createDiv = function(str, align, valign, overflow)
{
var div = this.createElement('div');
var state = this.state;
var css = '';
if (state.fontBackgroundColor != null)
{
css += 'background-color:' + mxUtils.htmlEntities(state.fontBackgroundColor) + ';';
}
if (state.fontBorderColor != null)
{
css += 'border:1px solid ' + mxUtils.htmlEntities(state.fontBorderColor) + ';';
}
if (mxUtils.isNode(str))
{
div.appendChild(str);
}
else
{
if (overflow != 'fill' && overflow != 'width')
{
var div2 = this.createElement('div');
div2.style.cssText = css;
div2.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
div2.style.zoom = '1';
div2.style.textDecoration = 'inherit';
div2.innerHTML = str;
div.appendChild(div2);
}
else
{
div.style.cssText = css;
div.innerHTML = str;
}
}
var style = div.style;
style.fontSize = (state.fontSize / this.vmlScale) + 'px';
style.fontFamily = state.fontFamily;
style.color = state.fontColor;
style.verticalAlign = 'top';
style.textAlign = align || 'left';
style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (state.fontSize * mxConstants.LINE_HEIGHT / this.vmlScale) + 'px' : mxConstants.LINE_HEIGHT;
if ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
style.fontWeight = 'bold';
}
if ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
style.fontStyle = 'italic';
}
if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
style.textDecoration = 'underline';
}
return div;
};
/**
* Function: text
*
* Paints the given text. Possible values for format are empty string for plain
* text and html for HTML markup. Clipping, text background and border are not
* supported for plain text in VML.
*/
mxVmlCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)
{
if (this.textEnabled && str != null)
{
var s = this.state;
if (format == 'html')
{
if (s.rotation != null)
{
var pt = this.rotatePoint(x, y, s.rotation, s.rotationCx, s.rotationCy);
x = pt.x;
y = pt.y;
}
if (document.documentMode == 8 && !mxClient.IS_EM)
{
x += s.dx;
y += s.dy;
// Workaround for rendering offsets
if (overflow != 'fill' && valign == mxConstants.ALIGN_TOP)
{
y -= 1;
}
}
else
{
x *= s.scale;
y *= s.scale;
}
// Adds event transparency in IE8 standards without the transparent background
// filter which cannot be used due to bugs in the zoomed bounding box (too slow)
// FIXME: No event transparency if inside v:rect (ie part of shape)
// KNOWN: Offset wrong for rotated text with word that are longer than the wrapping
// width in IE8 because real width of text cannot be determined here.
// This should be fixed in mxText.updateBoundingBox by calling before this and
// passing the real width to this method if not clipped and wrapped.
var abs = (document.documentMode == 8 && !mxClient.IS_EM) ? this.createVmlElement('group') : this.createElement('div');
abs.style.position = 'absolute';
abs.style.display = 'inline';
abs.style.left = this.format(x) + 'px';
abs.style.top = this.format(y) + 'px';
abs.style.zoom = s.scale;
var box = this.createElement('div');
box.style.position = 'relative';
box.style.display = 'inline';
var margin = mxUtils.getAlignmentAsPoint(align, valign);
var dx = margin.x;
var dy = margin.y;
var div = this.createDiv(str, align, valign, overflow);
var inner = this.createElement('div');
if (dir != null)
{
div.setAttribute('dir', dir);
}
if (wrap && w > 0)
{
if (!clip)
{
div.style.width = Math.round(w) + 'px';
}
div.style.wordWrap = mxConstants.WORD_WRAP;
div.style.whiteSpace = 'normal';
// LATER: Check if other cases need to be handled
if (div.style.wordWrap == 'break-word')
{
var tmp = div;
if (tmp.firstChild != null && tmp.firstChild.nodeName == 'DIV')
{
tmp.firstChild.style.width = '100%';
}
}
}
else
{
div.style.whiteSpace = 'nowrap';
}
var rot = s.rotation + (rotation || 0);
if (this.rotateHtml && rot != 0)
{
inner.style.display = 'inline';
inner.style.zoom = '1';
inner.appendChild(div);
// Box not needed for rendering in IE8 standards
if (document.documentMode == 8 && !mxClient.IS_EM && this.root.nodeName != 'DIV')
{
box.appendChild(inner);
abs.appendChild(box);
}
else
{
abs.appendChild(inner);
}
}
else if (document.documentMode == 8 && !mxClient.IS_EM)
{
box.appendChild(div);
abs.appendChild(box);
}
else
{
div.style.display = 'inline';
abs.appendChild(div);
}
// Inserts the node into the DOM
if (this.root.nodeName != 'DIV')
{
// Rectangle to fix position in group
var rect = this.createVmlElement('rect');
rect.stroked = 'false';
rect.filled = 'false';
rect.appendChild(abs);
this.root.appendChild(rect);
}
else
{
this.root.appendChild(abs);
}
if (clip)
{
div.style.overflow = 'hidden';
div.style.width = Math.round(w) + 'px';
if (!mxClient.IS_QUIRKS)
{
div.style.maxHeight = Math.round(h) + 'px';
}
}
else if (overflow == 'fill')
{
// KNOWN: Affects horizontal alignment in quirks
// but fill should only be used with align=left
div.style.overflow = 'hidden';
div.style.width = (Math.max(0, w) + 1) + 'px';
div.style.height = (Math.max(0, h) + 1) + 'px';
}
else if (overflow == 'width')
{
// KNOWN: Affects horizontal alignment in quirks
// but fill should only be used with align=left
div.style.overflow = 'hidden';
div.style.width = (Math.max(0, w) + 1) + 'px';
div.style.maxHeight = (Math.max(0, h) + 1) + 'px';
}
if (this.rotateHtml && rot != 0)
{
var rad = rot * (Math.PI / 180);
// Precalculate cos and sin for the rotation
var real_cos = parseFloat(parseFloat(Math.cos(rad)).toFixed(8));
var real_sin = parseFloat(parseFloat(Math.sin(-rad)).toFixed(8));
rad %= 2 * Math.PI;
if (rad < 0) rad += 2 * Math.PI;
rad %= Math.PI;
if (rad > Math.PI / 2) rad = Math.PI - rad;
var cos = Math.cos(rad);
var sin = Math.sin(rad);
// Adds div to document to measure size
if (document.documentMode == 8 && !mxClient.IS_EM)
{
div.style.display = 'inline-block';
inner.style.display = 'inline-block';
box.style.display = 'inline-block';
}
div.style.visibility = 'hidden';
div.style.position = 'absolute';
document.body.appendChild(div);
var sizeDiv = div;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
}
var tmp = sizeDiv.offsetWidth + 3;
var oh = sizeDiv.offsetHeight;
if (clip)
{
w = Math.min(w, tmp);
oh = Math.min(oh, h);
}
else
{
w = tmp;
}
// Handles words that are longer than the given wrapping width
if (wrap)
{
div.style.width = w + 'px';
}
// Simulates max-height in quirks
if (mxClient.IS_QUIRKS && (clip || overflow == 'width') && oh > h)
{
oh = h;
// Quirks does not support maxHeight
div.style.height = oh + 'px';
}
h = oh;
var top_fix = (h - h * cos + w * -sin) / 2 - real_sin * w * (dx + 0.5) + real_cos * h * (dy + 0.5);
var left_fix = (w - w * cos + h * -sin) / 2 + real_cos * w * (dx + 0.5) + real_sin * h * (dy + 0.5);
if (abs.nodeName == 'group' && this.root.nodeName == 'DIV')
{
// Workaround for bug where group gets moved away if left and top are non-zero in IE8 standards
var pos = this.createElement('div');
pos.style.display = 'inline-block';
pos.style.position = 'absolute';
pos.style.left = this.format(x + (left_fix - w / 2) * s.scale) + 'px';
pos.style.top = this.format(y + (top_fix - h / 2) * s.scale) + 'px';
abs.parentNode.appendChild(pos);
pos.appendChild(abs);
}
else
{
var sc = (document.documentMode == 8 && !mxClient.IS_EM) ? 1 : s.scale;
abs.style.left = this.format(x + (left_fix - w / 2) * sc) + 'px';
abs.style.top = this.format(y + (top_fix - h / 2) * sc) + 'px';
}
// KNOWN: Rotated text rendering quality is bad for IE9 quirks
inner.style.filter = "progid:DXImageTransform.Microsoft.Matrix(M11="+real_cos+", M12="+
real_sin+", M21="+(-real_sin)+", M22="+real_cos+", sizingMethod='auto expand')";
inner.style.backgroundColor = this.rotatedHtmlBackground;
if (this.state.alpha < 1)
{
inner.style.filter += 'alpha(opacity=' + (this.state.alpha * 100) + ')';
}
// Restore parent node for DIV
inner.appendChild(div);
div.style.position = '';
div.style.visibility = '';
}
else if (document.documentMode != 8 || mxClient.IS_EM)
{
div.style.verticalAlign = 'top';
if (this.state.alpha < 1)
{
abs.style.filter = 'alpha(opacity=' + (this.state.alpha * 100) + ')';
}
// Adds div to document to measure size
var divParent = div.parentNode;
div.style.visibility = 'hidden';
document.body.appendChild(div);
w = div.offsetWidth;
var oh = div.offsetHeight;
// Simulates max-height in quirks
if (mxClient.IS_QUIRKS && clip && oh > h)
{
oh = h;
// Quirks does not support maxHeight
div.style.height = oh + 'px';
}
h = oh;
div.style.visibility = '';
divParent.appendChild(div);
abs.style.left = this.format(x + w * dx * this.state.scale) + 'px';
abs.style.top = this.format(y + h * dy * this.state.scale) + 'px';
}
else
{
if (this.state.alpha < 1)
{
div.style.filter = 'alpha(opacity=' + (this.state.alpha * 100) + ')';
}
// Faster rendering in IE8 without offsetWidth/Height
box.style.left = (dx * 100) + '%';
box.style.top = (dy * 100) + '%';
}
}
else
{
this.plainText(x, y, w, h, mxUtils.htmlEntities(str, false), align, valign, wrap, format, overflow, clip, rotation, dir);
}
}
};
/**
* Function: plainText
*
* Paints the outline of the current path.
*/
mxVmlCanvas2D.prototype.plainText = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)
{
// TextDirection is ignored since this code is not used (format is always HTML in the text function)
var s = this.state;
x = (x + s.dx) * s.scale;
y = (y + s.dy) * s.scale;
var node = this.createVmlElement('shape');
node.style.width = '1px';
node.style.height = '1px';
node.stroked = 'false';
var fill = this.createVmlElement('fill');
fill.color = s.fontColor;
fill.opacity = (s.alpha * 100) + '%';
node.appendChild(fill);
var path = this.createVmlElement('path');
path.textpathok = 'true';
path.v = 'm ' + this.format(0) + ' ' + this.format(0) + ' l ' + this.format(1) + ' ' + this.format(0);
node.appendChild(path);
// KNOWN: Font family and text decoration ignored
var tp = this.createVmlElement('textpath');
tp.style.cssText = 'v-text-align:' + align;
tp.style.align = align;
tp.style.fontFamily = s.fontFamily;
tp.string = str;
tp.on = 'true';
// Scale via fontsize instead of node.style.zoom for correct offsets in IE8
var size = s.fontSize * s.scale / this.vmlScale;
tp.style.fontSize = size + 'px';
// Bold
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
tp.style.fontWeight = 'bold';
}
// Italic
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
tp.style.fontStyle = 'italic';
}
// Underline
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
tp.style.textDecoration = 'underline';
}
var lines = str.split('\n');
var textHeight = size + (lines.length - 1) * size * mxConstants.LINE_HEIGHT;
var dx = 0;
var dy = 0;
if (valign == mxConstants.ALIGN_BOTTOM)
{
dy = - textHeight / 2;
}
else if (valign != mxConstants.ALIGN_MIDDLE) // top
{
dy = textHeight / 2;
}
if (rotation != null)
{
node.style.rotation = rotation;
var rad = rotation * (Math.PI / 180);
dx = Math.sin(rad) * dy;
dy = Math.cos(rad) * dy;
}
// FIXME: Clipping is relative to bounding box
/*if (clip)
{
node.style.clip = 'rect(0px ' + this.format(w) + 'px ' + this.format(h) + 'px 0px)';
}*/
node.appendChild(tp);
node.style.left = this.format(x - dx) + 'px';
node.style.top = this.format(y + dy) + 'px';
this.root.appendChild(node);
};
/**
* Function: stroke
*
* Paints the outline of the current path.
*/
mxVmlCanvas2D.prototype.stroke = function()
{
this.addNode(false, true);
};
/**
* Function: fill
*
* Fills the current path.
*/
mxVmlCanvas2D.prototype.fill = function()
{
this.addNode(true, false);
};
/**
* Function: fillAndStroke
*
* Fills and paints the outline of the current path.
*/
mxVmlCanvas2D.prototype.fillAndStroke = function()
{
this.addNode(true, true);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGuide
*
* Implements the alignment of selection cells to other cells in the graph.
*
* Constructor: mxGuide
*
* Constructs a new guide object.
*/
function mxGuide(graph, states)
{
this.graph = graph;
this.setStates(states);
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph> instance.
*/
mxGuide.prototype.graph = null;
/**
* Variable: states
*
* Contains the <mxCellStates> that are used for alignment.
*/
mxGuide.prototype.states = null;
/**
* Variable: horizontal
*
* Specifies if horizontal guides are enabled. Default is true.
*/
mxGuide.prototype.horizontal = true;
/**
* Variable: vertical
*
* Specifies if vertical guides are enabled. Default is true.
*/
mxGuide.prototype.vertical = true;
/**
* Variable: vertical
*
* Holds the <mxShape> for the horizontal guide.
*/
mxGuide.prototype.guideX = null;
/**
* Variable: vertical
*
* Holds the <mxShape> for the vertical guide.
*/
mxGuide.prototype.guideY = null;
/**
* Variable: rounded
*
* Specifies if rounded coordinates should be used. Default is false.
*/
mxGuide.prototype.rounded = false;
/**
* Function: setStates
*
* Sets the <mxCellStates> that should be used for alignment.
*/
mxGuide.prototype.setStates = function(states)
{
this.states = states;
};
/**
* Function: isEnabledForEvent
*
* Returns true if the guide should be enabled for the given native event. This
* implementation always returns true.
*/
mxGuide.prototype.isEnabledForEvent = function(evt)
{
return true;
};
/**
* Function: getGuideTolerance
*
* Returns the tolerance for the guides. Default value is gridSize / 2.
*/
mxGuide.prototype.getGuideTolerance = function()
{
return this.graph.gridSize / 2;
};
/**
* Function: createGuideShape
*
* Returns the mxShape to be used for painting the respective guide. This
* implementation returns a new, dashed and crisp <mxPolyline> using
* <mxConstants.GUIDE_COLOR> and <mxConstants.GUIDE_STROKEWIDTH> as the format.
*
* Parameters:
*
* horizontal - Boolean that specifies which guide should be created.
*/
mxGuide.prototype.createGuideShape = function(horizontal)
{
var guide = new mxPolyline([], mxConstants.GUIDE_COLOR, mxConstants.GUIDE_STROKEWIDTH);
guide.isDashed = true;
return guide;
};
/**
* Function: isStateIgnored
*
* Returns true if the given state should be ignored.
*/
mxGuide.prototype.isStateIgnored = function(state)
{
return false;
};
/**
* Function: move
*
* Moves the <bounds> by the given <mxPoint> and returnt the snapped point.
*/
mxGuide.prototype.move = function(bounds, delta, gridEnabled, clone)
{
if (this.states != null && (this.horizontal || this.vertical) && bounds != null && delta != null)
{
var trx = this.graph.getView().translate;
var scale = this.graph.getView().scale;
var dx = delta.x;
var dy = delta.y;
var overrideX = false;
var stateX = null;
var valueX = null;
var overrideY = false;
var stateY = null;
var valueY = null;
var tt = this.getGuideTolerance();
var ttX = tt;
var ttY = tt;
var b = bounds.clone();
b.x += delta.x;
b.y += delta.y;
var left = b.x;
var right = b.x + b.width;
var center = b.getCenterX();
var top = b.y;
var bottom = b.y + b.height;
var middle = b.getCenterY();
// Snaps the left, center and right to the given x-coordinate
function snapX(x, state)
{
x += this.graph.panDx;
var override = false;
if (Math.abs(x - center) < ttX)
{
dx = x - bounds.getCenterX();
ttX = Math.abs(x - center);
override = true;
}
else if (Math.abs(x - left) < ttX)
{
dx = x - bounds.x;
ttX = Math.abs(x - left);
override = true;
}
else if (Math.abs(x - right) < ttX)
{
dx = x - bounds.x - bounds.width;
ttX = Math.abs(x - right);
override = true;
}
if (override)
{
stateX = state;
valueX = Math.round(x - this.graph.panDx);
if (this.guideX == null)
{
this.guideX = this.createGuideShape(true);
// Makes sure to use either VML or SVG shapes in order to implement
// event-transparency on the background area of the rectangle since
// HTML shapes do not let mouseevents through even when transparent
this.guideX.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.guideX.pointerEvents = false;
this.guideX.init(this.graph.getView().getOverlayPane());
}
}
overrideX = overrideX || override;
};
// Snaps the top, middle or bottom to the given y-coordinate
function snapY(y, state)
{
y += this.graph.panDy;
var override = false;
if (Math.abs(y - middle) < ttY)
{
dy = y - bounds.getCenterY();
ttY = Math.abs(y - middle);
override = true;
}
else if (Math.abs(y - top) < ttY)
{
dy = y - bounds.y;
ttY = Math.abs(y - top);
override = true;
}
else if (Math.abs(y - bottom) < ttY)
{
dy = y - bounds.y - bounds.height;
ttY = Math.abs(y - bottom);
override = true;
}
if (override)
{
stateY = state;
valueY = Math.round(y - this.graph.panDy);
if (this.guideY == null)
{
this.guideY = this.createGuideShape(false);
// Makes sure to use either VML or SVG shapes in order to implement
// event-transparency on the background area of the rectangle since
// HTML shapes do not let mouseevents through even when transparent
this.guideY.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.guideY.pointerEvents = false;
this.guideY.init(this.graph.getView().getOverlayPane());
}
}
overrideY = overrideY || override;
};
for (var i = 0; i < this.states.length; i++)
{
var state = this.states[i];
if (state != null && !this.isStateIgnored(state))
{
// Align x
if (this.horizontal)
{
snapX.call(this, state.getCenterX(), state);
snapX.call(this, state.x, state);
snapX.call(this, state.x + state.width, state);
}
// Align y
if (this.vertical)
{
snapY.call(this, state.getCenterY(), state);
snapY.call(this, state.y, state);
snapY.call(this, state.y + state.height, state);
}
}
}
// Moves cells that are off-grid back to the grid on move
if (gridEnabled)
{
if (!overrideX)
{
var tx = bounds.x - (this.graph.snap(bounds.x /
scale - trx.x) + trx.x) * scale;
dx = this.graph.snap(dx / scale) * scale - tx;
}
if (!overrideY)
{
var ty = bounds.y - (this.graph.snap(bounds.y /
scale - trx.y) + trx.y) * scale;
dy = this.graph.snap(dy / scale) * scale - ty;
}
}
// Redraws the guides
var c = this.graph.container;
if (!overrideX && this.guideX != null)
{
this.guideX.node.style.visibility = 'hidden';
}
else if (this.guideX != null)
{
var minY = null;
var maxY = null;
if (stateX != null && bounds != null)
{
minY = Math.min(bounds.y + dy - this.graph.panDy, stateX.y);
maxY = Math.max(bounds.y + bounds.height + dy - this.graph.panDy, stateX.y + stateX.height);
}
if (minY != null && maxY != null)
{
this.guideX.points = [new mxPoint(valueX, minY), new mxPoint(valueX, maxY)];
}
else
{
this.guideX.points = [new mxPoint(valueX, -this.graph.panDy), new mxPoint(valueX, c.scrollHeight - 3 - this.graph.panDy)];
}
this.guideX.stroke = this.getGuideColor(stateX, true);
this.guideX.node.style.visibility = 'visible';
this.guideX.redraw();
}
if (!overrideY && this.guideY != null)
{
this.guideY.node.style.visibility = 'hidden';
}
else if (this.guideY != null)
{
var minX = null;
var maxX = null;
if (stateY != null && bounds != null)
{
minX = Math.min(bounds.x + dx - this.graph.panDx, stateY.x);
maxX = Math.max(bounds.x + bounds.width + dx - this.graph.panDx, stateY.x + stateY.width);
}
if (minX != null && maxX != null)
{
this.guideY.points = [new mxPoint(minX, valueY), new mxPoint(maxX, valueY)];
}
else
{
this.guideY.points = [new mxPoint(-this.graph.panDx, valueY), new mxPoint(c.scrollWidth - 3 - this.graph.panDx, valueY)];
}
this.guideY.stroke = this.getGuideColor(stateY, false);
this.guideY.node.style.visibility = 'visible';
this.guideY.redraw();
}
delta = this.getDelta(bounds, stateX, dx, stateY, dy)
}
return delta;
};
/**
* Function: hide
*
* Hides all current guides.
*/
mxGuide.prototype.getDelta = function(bounds, stateX, dx, stateY, dy)
{
// Round to pixels for virtual states (eg. page guides)
if (this.rounded || (stateX != null && stateX.cell == null))
{
dx = Math.floor(bounds.x + dx) - bounds.x;
}
if (this.rounded || (stateY != null && stateY.cell == null))
{
dy = Math.floor(bounds.y + dy) - bounds.y;
}
return new mxPoint(dx, dy);
};
/**
* Function: hide
*
* Hides all current guides.
*/
mxGuide.prototype.getGuideColor = function(state, horizontal)
{
return mxConstants.GUIDE_COLOR;
};
/**
* Function: hide
*
* Hides all current guides.
*/
mxGuide.prototype.hide = function()
{
this.setVisible(false);
};
/**
* Function: setVisible
*
* Shows or hides the current guides.
*/
mxGuide.prototype.setVisible = function(visible)
{
if (this.guideX != null)
{
this.guideX.node.style.visibility = (visible) ? 'visible' : 'hidden';
}
if (this.guideY != null)
{
this.guideY.node.style.visibility = (visible) ? 'visible' : 'hidden';
}
};
/**
* Function: destroy
*
* Destroys all resources that this object uses.
*/
mxGuide.prototype.destroy = function()
{
if (this.guideX != null)
{
this.guideX.destroy();
this.guideX = null;
}
if (this.guideY != null)
{
this.guideY.destroy();
this.guideY = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxShape
*
* Base class for all shapes. A shape in mxGraph is a
* separate implementation for SVG, VML and HTML. Which
* implementation to use is controlled by the <dialect>
* property which is assigned from within the <mxCellRenderer>
* when the shape is created. The dialect must be assigned
* for a shape, and it does normally depend on the browser and
* the confiuration of the graph (see <mxGraph> rendering hint).
*
* For each supported shape in SVG and VML, a corresponding
* shape exists in mxGraph, namely for text, image, rectangle,
* rhombus, ellipse and polyline. The other shapes are a
* combination of these shapes (eg. label and swimlane)
* or they consist of one or more (filled) path objects
* (eg. actor and cylinder). The HTML implementation is
* optional but may be required for a HTML-only view of
* the graph.
*
* Custom Shapes:
*
* To extend from this class, the basic code looks as follows.
* In the special case where the custom shape consists only of
* one filled region or one filled region and an additional stroke
* the <mxActor> and <mxCylinder> should be subclassed,
* respectively.
*
* (code)
* function CustomShape() { }
*
* CustomShape.prototype = new mxShape();
* CustomShape.prototype.constructor = CustomShape;
* (end)
*
* To register a custom shape in an existing graph instance,
* one must register the shape under a new name in the graph's
* cell renderer as follows:
*
* (code)
* mxCellRenderer.registerShape('customShape', CustomShape);
* (end)
*
* The second argument is the name of the constructor.
*
* In order to use the shape you can refer to the given name above
* in a stylesheet. For example, to change the shape for the default
* vertex style, the following code is used:
*
* (code)
* var style = graph.getStylesheet().getDefaultVertexStyle();
* style[mxConstants.STYLE_SHAPE] = 'customShape';
* (end)
*
* Constructor: mxShape
*
* Constructs a new shape.
*/
function mxShape(stencil)
{
this.stencil = stencil;
this.initStyles();
};
/**
* Variable: dialect
*
* Holds the dialect in which the shape is to be painted.
* This can be one of the DIALECT constants in <mxConstants>.
*/
mxShape.prototype.dialect = null;
/**
* Variable: scale
*
* Holds the scale in which the shape is being painted.
*/
mxShape.prototype.scale = 1;
/**
* Variable: antiAlias
*
* Rendering hint for configuring the canvas.
*/
mxShape.prototype.antiAlias = true;
/**
* Variable: minSvgStrokeWidth
*
* Minimum stroke width for SVG output.
*/
mxShape.prototype.minSvgStrokeWidth = 1;
/**
* Variable: bounds
*
* Holds the <mxRectangle> that specifies the bounds of this shape.
*/
mxShape.prototype.bounds = null;
/**
* Variable: points
*
* Holds the array of <mxPoints> that specify the points of this shape.
*/
mxShape.prototype.points = null;
/**
* Variable: node
*
* Holds the outermost DOM node that represents this shape.
*/
mxShape.prototype.node = null;
/**
* Variable: state
*
* Optional reference to the corresponding <mxCellState>.
*/
mxShape.prototype.state = null;
/**
* Variable: style
*
* Optional reference to the style of the corresponding <mxCellState>.
*/
mxShape.prototype.style = null;
/**
* Variable: boundingBox
*
* Contains the bounding box of the shape, that is, the smallest rectangle
* that includes all pixels of the shape.
*/
mxShape.prototype.boundingBox = null;
/**
* Variable: stencil
*
* Holds the <mxStencil> that defines the shape.
*/
mxShape.prototype.stencil = null;
/**
* Variable: svgStrokeTolerance
*
* Event-tolerance for SVG strokes (in px). Default is 8. This is only passed
* to the canvas in <createSvgCanvas> if <pointerEvents> is true.
*/
mxShape.prototype.svgStrokeTolerance = 8;
/**
* Variable: pointerEvents
*
* Specifies if pointer events should be handled. Default is true.
*/
mxShape.prototype.pointerEvents = true;
/**
* Variable: svgPointerEvents
*
* Specifies if pointer events should be handled. Default is true.
*/
mxShape.prototype.svgPointerEvents = 'all';
/**
* Variable: shapePointerEvents
*
* Specifies if pointer events outside of shape should be handled. Default
* is false.
*/
mxShape.prototype.shapePointerEvents = false;
/**
* Variable: stencilPointerEvents
*
* Specifies if pointer events outside of stencils should be handled. Default
* is false. Set this to true for backwards compatibility with the 1.x branch.
*/
mxShape.prototype.stencilPointerEvents = false;
/**
* Variable: vmlScale
*
* Scale for improving the precision of VML rendering. Default is 1.
*/
mxShape.prototype.vmlScale = 1;
/**
* Variable: outline
*
* Specifies if the shape should be drawn as an outline. This disables all
* fill colors and can be used to disable other drawing states that should
* not be painted for outlines. Default is false. This should be set before
* calling <apply>.
*/
mxShape.prototype.outline = false;
/**
* Variable: visible
*
* Specifies if the shape is visible. Default is true.
*/
mxShape.prototype.visible = true;
/**
* Variable: useSvgBoundingBox
*
* Allows to use the SVG bounding box in SVG. Default is false for performance
* reasons.
*/
mxShape.prototype.useSvgBoundingBox = false;
/**
* Function: init
*
* Initializes the shape by creaing the DOM node using <create>
* and adding it into the given container.
*
* Parameters:
*
* container - DOM node that will contain the shape.
*/
mxShape.prototype.init = function(container)
{
if (this.node == null)
{
this.node = this.create(container);
if (container != null)
{
container.appendChild(this.node);
}
}
};
/**
* Function: initStyles
*
* Sets the styles to their default values.
*/
mxShape.prototype.initStyles = function(container)
{
this.strokewidth = 1;
this.rotation = 0;
this.opacity = 100;
this.fillOpacity = 100;
this.strokeOpacity = 100;
this.flipH = false;
this.flipV = false;
};
/**
* Function: isParseVml
*
* Specifies if any VML should be added via insertAdjacentHtml to the DOM. This
* is only needed in IE8 and only if the shape contains VML markup. This method
* returns true.
*/
mxShape.prototype.isParseVml = function()
{
return true;
};
/**
* Function: isHtmlAllowed
*
* Returns true if HTML is allowed for this shape. This implementation always
* returns false.
*/
mxShape.prototype.isHtmlAllowed = function()
{
return false;
};
/**
* Function: getSvgScreenOffset
*
* Returns 0, or 0.5 if <strokewidth> % 2 == 1.
*/
mxShape.prototype.getSvgScreenOffset = function()
{
var sw = this.stencil && this.stencil.strokewidth != 'inherit' ? Number(this.stencil.strokewidth) : this.strokewidth;
return (mxUtils.mod(Math.max(1, Math.round(sw * this.scale)), 2) == 1) ? 0.5 : 0;
};
/**
* Function: create
*
* Creates and returns the DOM node(s) for the shape in
* the given container. This implementation invokes
* <createSvg>, <createHtml> or <createVml> depending
* on the <dialect> and style settings.
*
* Parameters:
*
* container - DOM node that will contain the shape.
*/
mxShape.prototype.create = function(container)
{
var node = null;
if (container != null && container.ownerSVGElement != null)
{
node = this.createSvg(container);
}
else if (document.documentMode == 8 || !mxClient.IS_VML ||
(this.dialect != mxConstants.DIALECT_VML && this.isHtmlAllowed()))
{
node = this.createHtml(container);
}
else
{
node = this.createVml(container);
}
return node;
};
/**
* Function: createSvg
*
* Creates and returns the SVG node(s) to represent this shape.
*/
mxShape.prototype.createSvg = function()
{
return document.createElementNS(mxConstants.NS_SVG, 'g');
};
/**
* Function: createVml
*
* Creates and returns the VML node to represent this shape.
*/
mxShape.prototype.createVml = function()
{
var node = document.createElement(mxClient.VML_PREFIX + ':group');
node.style.position = 'absolute';
return node;
};
/**
* Function: createHtml
*
* Creates and returns the HTML DOM node(s) to represent
* this shape. This implementation falls back to <createVml>
* so that the HTML creation is optional.
*/
mxShape.prototype.createHtml = function()
{
var node = document.createElement('div');
node.style.position = 'absolute';
return node;
};
/**
* Function: reconfigure
*
* Reconfigures this shape. This will update the colors etc in
* addition to the bounds or points.
*/
mxShape.prototype.reconfigure = function()
{
this.redraw();
};
/**
* Function: redraw
*
* Creates and returns the SVG node(s) to represent this shape.
*/
mxShape.prototype.redraw = function()
{
this.updateBoundsFromPoints();
if (this.visible && this.checkBounds())
{
this.node.style.visibility = 'visible';
this.clear();
if (this.node.nodeName == 'DIV' && (this.isHtmlAllowed() || !mxClient.IS_VML))
{
this.redrawHtmlShape();
}
else
{
this.redrawShape();
}
this.updateBoundingBox();
}
else
{
this.node.style.visibility = 'hidden';
this.boundingBox = null;
}
};
/**
* Function: clear
*
* Removes all child nodes and resets all CSS.
*/
mxShape.prototype.clear = function()
{
if (this.node.ownerSVGElement != null)
{
while (this.node.lastChild != null)
{
this.node.removeChild(this.node.lastChild);
}
}
else
{
this.node.style.cssText = 'position:absolute;' + ((this.cursor != null) ?
('cursor:' + this.cursor + ';') : '');
this.node.innerHTML = '';
}
};
/**
* Function: updateBoundsFromPoints
*
* Updates the bounds based on the points.
*/
mxShape.prototype.updateBoundsFromPoints = function()
{
var pts = this.points;
if (pts != null && pts.length > 0 && pts[0] != null)
{
this.bounds = new mxRectangle(Number(pts[0].x), Number(pts[0].y), 1, 1);
for (var i = 1; i < this.points.length; i++)
{
if (pts[i] != null)
{
this.bounds.add(new mxRectangle(Number(pts[i].x), Number(pts[i].y), 1, 1));
}
}
}
};
/**
* Function: getLabelBounds
*
* Returns the <mxRectangle> for the label bounds of this shape, based on the
* given scaled and translated bounds of the shape. This method should not
* change the rectangle in-place. This implementation returns the given rect.
*/
mxShape.prototype.getLabelBounds = function(rect)
{
var d = mxUtils.getValue(this.style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST);
var bounds = rect;
// Normalizes argument for getLabelMargins hook
if (d != mxConstants.DIRECTION_SOUTH && d != mxConstants.DIRECTION_NORTH &&
this.state != null && this.state.text != null &&
this.state.text.isPaintBoundsInverted())
{
bounds = bounds.clone();
var tmp = bounds.width;
bounds.width = bounds.height;
bounds.height = tmp;
}
var m = this.getLabelMargins(bounds);
if (m != null)
{
var flipH = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPH, false) == '1';
var flipV = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPV, false) == '1';
// Handles special case for vertical labels
if (this.state != null && this.state.text != null &&
this.state.text.isPaintBoundsInverted())
{
var tmp = m.x;
m.x = m.height;
m.height = m.width;
m.width = m.y;
m.y = tmp;
tmp = flipH;
flipH = flipV;
flipV = tmp;
}
return mxUtils.getDirectedBounds(rect, m, this.style, flipH, flipV);
}
return rect;
};
/**
* Function: getLabelMargins
*
* Returns the scaled top, left, bottom and right margin to be used for
* computing the label bounds as an <mxRectangle>, where the bottom and right
* margin are defined in the width and height of the rectangle, respectively.
*/
mxShape.prototype.getLabelMargins= function(rect)
{
return null;
};
/**
* Function: checkBounds
*
* Returns true if the bounds are not null and all of its variables are numeric.
*/
mxShape.prototype.checkBounds = function()
{
return (!isNaN(this.scale) && isFinite(this.scale) && this.scale > 0 &&
this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) &&
!isNaN(this.bounds.width) && !isNaN(this.bounds.height) &&
this.bounds.width > 0 && this.bounds.height > 0);
};
/**
* Function: createVmlGroup
*
* Returns the temporary element used for rendering in IE8 standards mode.
*/
mxShape.prototype.createVmlGroup = function()
{
var node = document.createElement(mxClient.VML_PREFIX + ':group');
node.style.position = 'absolute';
node.style.width = this.node.style.width;
node.style.height = this.node.style.height;
return node;
};
/**
* Function: redrawShape
*
* Updates the SVG or VML shape.
*/
mxShape.prototype.redrawShape = function()
{
var canvas = this.createCanvas();
if (canvas != null)
{
// Specifies if events should be handled
canvas.pointerEvents = this.pointerEvents;
this.paint(canvas);
if (this.node != canvas.root)
{
// Forces parsing in IE8 standards mode - slow! avoid
this.node.insertAdjacentHTML('beforeend', canvas.root.outerHTML);
}
if (this.node.nodeName == 'DIV' && document.documentMode == 8)
{
// Makes DIV transparent to events for IE8 in IE8 standards
// mode (Note: Does not work for IE9 in IE8 standards mode
// and not for IE11 in enterprise mode)
this.node.style.filter = '';
// Adds event transparency in IE8 standards
mxUtils.addTransparentBackgroundFilter(this.node);
}
this.destroyCanvas(canvas);
}
};
/**
* Function: createCanvas
*
* Creates a new canvas for drawing this shape. May return null.
*/
mxShape.prototype.createCanvas = function()
{
var canvas = null;
// LATER: Check if reusing existing DOM nodes improves performance
if (this.node.ownerSVGElement != null)
{
canvas = this.createSvgCanvas();
}
else if (mxClient.IS_VML)
{
this.updateVmlContainer();
canvas = this.createVmlCanvas();
}
if (canvas != null && this.outline)
{
canvas.setStrokeWidth(this.strokewidth);
canvas.setStrokeColor(this.stroke);
if (this.isDashed != null)
{
canvas.setDashed(this.isDashed);
}
canvas.setStrokeWidth = function() {};
canvas.setStrokeColor = function() {};
canvas.setFillColor = function() {};
canvas.setGradient = function() {};
canvas.setDashed = function() {};
canvas.text = function() {};
}
return canvas;
};
/**
* Function: createSvgCanvas
*
* Creates and returns an <mxSvgCanvas2D> for rendering this shape.
*/
mxShape.prototype.createSvgCanvas = function()
{
var canvas = new mxSvgCanvas2D(this.node, false);
canvas.strokeTolerance = (this.pointerEvents) ? this.svgStrokeTolerance : 0;
canvas.pointerEventsValue = this.svgPointerEvents;
canvas.blockImagePointerEvents = mxClient.IS_FF;
var off = this.getSvgScreenOffset();
if (off != 0)
{
this.node.setAttribute('transform', 'translate(' + off + ',' + off + ')');
}
else
{
this.node.removeAttribute('transform');
}
canvas.minStrokeWidth = this.minSvgStrokeWidth;
if (!this.antiAlias)
{
// Rounds all numbers in the SVG output to integers
canvas.format = function(value)
{
return Math.round(parseFloat(value));
};
}
return canvas;
};
/**
* Function: createVmlCanvas
*
* Creates and returns an <mxVmlCanvas2D> for rendering this shape.
*/
mxShape.prototype.createVmlCanvas = function()
{
// Workaround for VML rendering bug in IE8 standards mode
var node = (document.documentMode == 8 && this.isParseVml()) ? this.createVmlGroup() : this.node;
var canvas = new mxVmlCanvas2D(node, false);
if (node.tagUrn != '')
{
var w = Math.max(1, Math.round(this.bounds.width));
var h = Math.max(1, Math.round(this.bounds.height));
node.coordsize = (w * this.vmlScale) + ',' + (h * this.vmlScale);
canvas.scale(this.vmlScale);
canvas.vmlScale = this.vmlScale;
}
// Painting relative to top, left shape corner
var s = this.scale;
canvas.translate(-Math.round(this.bounds.x / s), -Math.round(this.bounds.y / s));
return canvas;
};
/**
* Function: updateVmlContainer
*
* Updates the bounds of the VML container.
*/
mxShape.prototype.updateVmlContainer = function()
{
this.node.style.left = Math.round(this.bounds.x) + 'px';
this.node.style.top = Math.round(this.bounds.y) + 'px';
var w = Math.max(1, Math.round(this.bounds.width));
var h = Math.max(1, Math.round(this.bounds.height));
this.node.style.width = w + 'px';
this.node.style.height = h + 'px';
this.node.style.overflow = 'visible';
};
/**
* Function: redrawHtml
*
* Allow optimization by replacing VML with HTML.
*/
mxShape.prototype.redrawHtmlShape = function()
{
// LATER: Refactor methods
this.updateHtmlBounds(this.node);
this.updateHtmlFilters(this.node);
this.updateHtmlColors(this.node);
};
/**
* Function: updateHtmlFilters
*
* Allow optimization by replacing VML with HTML.
*/
mxShape.prototype.updateHtmlFilters = function(node)
{
var f = '';
if (this.opacity < 100)
{
f += 'alpha(opacity=' + (this.opacity) + ')';
}
if (this.isShadow)
{
// FIXME: Cannot implement shadow transparency with filter
f += 'progid:DXImageTransform.Microsoft.dropShadow (' +
'OffX=\'' + Math.round(mxConstants.SHADOW_OFFSET_X * this.scale) + '\', ' +
'OffY=\'' + Math.round(mxConstants.SHADOW_OFFSET_Y * this.scale) + '\', ' +
'Color=\'' + mxConstants.VML_SHADOWCOLOR + '\')';
}
if (this.fill != null && this.fill != mxConstants.NONE && this.gradient && this.gradient != mxConstants.NONE)
{
var start = this.fill;
var end = this.gradient;
var type = '0';
var lookup = {east:0,south:1,west:2,north:3};
var dir = (this.direction != null) ? lookup[this.direction] : 0;
if (this.gradientDirection != null)
{
dir = mxUtils.mod(dir + lookup[this.gradientDirection] - 1, 4);
}
if (dir == 1)
{
type = '1';
var tmp = start;
start = end;
end = tmp;
}
else if (dir == 2)
{
var tmp = start;
start = end;
end = tmp;
}
else if (dir == 3)
{
type = '1';
}
f += 'progid:DXImageTransform.Microsoft.gradient(' +
'startColorStr=\'' + start + '\', endColorStr=\'' + end +
'\', gradientType=\'' + type + '\')';
}
node.style.filter = f;
};
/**
* Function: mixedModeHtml
*
* Allow optimization by replacing VML with HTML.
*/
mxShape.prototype.updateHtmlColors = function(node)
{
var color = this.stroke;
if (color != null && color != mxConstants.NONE)
{
node.style.borderColor = color;
if (this.isDashed)
{
node.style.borderStyle = 'dashed';
}
else if (this.strokewidth > 0)
{
node.style.borderStyle = 'solid';
}
node.style.borderWidth = Math.max(1, Math.ceil(this.strokewidth * this.scale)) + 'px';
}
else
{
node.style.borderWidth = '0px';
}
color = (this.outline) ? null : this.fill;
if (color != null && color != mxConstants.NONE)
{
node.style.backgroundColor = color;
node.style.backgroundImage = 'none';
}
else if (this.pointerEvents)
{
node.style.backgroundColor = 'transparent';
}
else if (document.documentMode == 8)
{
mxUtils.addTransparentBackgroundFilter(node);
}
else
{
this.setTransparentBackgroundImage(node);
}
};
/**
* Function: mixedModeHtml
*
* Allow optimization by replacing VML with HTML.
*/
mxShape.prototype.updateHtmlBounds = function(node)
{
var sw = (document.documentMode >= 9) ? 0 : Math.ceil(this.strokewidth * this.scale);
node.style.borderWidth = Math.max(1, sw) + 'px';
node.style.overflow = 'hidden';
node.style.left = Math.round(this.bounds.x - sw / 2) + 'px';
node.style.top = Math.round(this.bounds.y - sw / 2) + 'px';
if (document.compatMode == 'CSS1Compat')
{
sw = -sw;
}
node.style.width = Math.round(Math.max(0, this.bounds.width + sw)) + 'px';
node.style.height = Math.round(Math.max(0, this.bounds.height + sw)) + 'px';
};
/**
* Function: destroyCanvas
*
* Destroys the given canvas which was used for drawing. This implementation
* increments the reference counts on all shared gradients used in the canvas.
*/
mxShape.prototype.destroyCanvas = function(canvas)
{
// Manages reference counts
if (canvas instanceof mxSvgCanvas2D)
{
// Increments ref counts
for (var key in canvas.gradients)
{
var gradient = canvas.gradients[key];
if (gradient != null)
{
gradient.mxRefCount = (gradient.mxRefCount || 0) + 1;
}
}
this.releaseSvgGradients(this.oldGradients);
this.oldGradients = canvas.gradients;
}
};
/**
* Function: paint
*
* Generic rendering code.
*/
mxShape.prototype.paint = function(c)
{
var strokeDrawn = false;
if (c != null && this.outline)
{
var stroke = c.stroke;
c.stroke = function()
{
strokeDrawn = true;
stroke.apply(this, arguments);
};
var fillAndStroke = c.fillAndStroke;
c.fillAndStroke = function()
{
strokeDrawn = true;
fillAndStroke.apply(this, arguments);
};
}
// Scale is passed-through to canvas
var s = this.scale;
var x = this.bounds.x / s;
var y = this.bounds.y / s;
var w = this.bounds.width / s;
var h = this.bounds.height / s;
if (this.isPaintBoundsInverted())
{
var t = (w - h) / 2;
x += t;
y -= t;
var tmp = w;
w = h;
h = tmp;
}
this.updateTransform(c, x, y, w, h);
this.configureCanvas(c, x, y, w, h);
// Adds background rectangle to capture events
var bg = null;
if ((this.stencil == null && this.points == null && this.shapePointerEvents) ||
(this.stencil != null && this.stencilPointerEvents))
{
var bb = this.createBoundingBox();
if (this.dialect == mxConstants.DIALECT_SVG)
{
bg = this.createTransparentSvgRectangle(bb.x, bb.y, bb.width, bb.height);
this.node.appendChild(bg);
}
else
{
var rect = c.createRect('rect', bb.x / s, bb.y / s, bb.width / s, bb.height / s);
rect.appendChild(c.createTransparentFill());
rect.stroked = 'false';
c.root.appendChild(rect);
}
}
if (this.stencil != null)
{
this.stencil.drawShape(c, this, x, y, w, h);
}
else
{
// Stencils have separate strokewidth
c.setStrokeWidth(this.strokewidth);
if (this.points != null)
{
// Paints edge shape
var pts = [];
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
pts.push(new mxPoint(this.points[i].x / s, this.points[i].y / s));
}
}
this.paintEdgeShape(c, pts);
}
else
{
// Paints vertex shape
this.paintVertexShape(c, x, y, w, h);
}
}
if (bg != null && c.state != null && c.state.transform != null)
{
bg.setAttribute('transform', c.state.transform);
}
// Draws highlight rectangle if no stroke was used
if (c != null && this.outline && !strokeDrawn)
{
c.rect(x, y, w, h);
c.stroke();
}
};
/**
* Function: configureCanvas
*
* Sets the state of the canvas for drawing the shape.
*/
mxShape.prototype.configureCanvas = function(c, x, y, w, h)
{
var dash = null;
if (this.style != null)
{
dash = this.style['dashPattern'];
}
c.setAlpha(this.opacity / 100);
c.setFillAlpha(this.fillOpacity / 100);
c.setStrokeAlpha(this.strokeOpacity / 100);
// Sets alpha, colors and gradients
if (this.isShadow != null)
{
c.setShadow(this.isShadow);
}
// Dash pattern
if (this.isDashed != null)
{
c.setDashed(this.isDashed, (this.style != null) ?
mxUtils.getValue(this.style, mxConstants.STYLE_FIX_DASH, false) == 1 : false);
}
if (dash != null)
{
c.setDashPattern(dash);
}
if (this.fill != null && this.fill != mxConstants.NONE && this.gradient && this.gradient != mxConstants.NONE)
{
var b = this.getGradientBounds(c, x, y, w, h);
c.setGradient(this.fill, this.gradient, b.x, b.y, b.width, b.height, this.gradientDirection);
}
else
{
c.setFillColor(this.fill);
}
c.setStrokeColor(this.stroke);
};
/**
* Function: getGradientBounds
*
* Returns the bounding box for the gradient box for this shape.
*/
mxShape.prototype.getGradientBounds = function(c, x, y, w, h)
{
return new mxRectangle(x, y, w, h);
};
/**
* Function: updateTransform
*
* Sets the scale and rotation on the given canvas.
*/
mxShape.prototype.updateTransform = function(c, x, y, w, h)
{
// NOTE: Currently, scale is implemented in state and canvas. This will
// move to canvas in a later version, so that the states are unscaled
// and untranslated and do not need an update after zooming or panning.
c.scale(this.scale);
c.rotate(this.getShapeRotation(), this.flipH, this.flipV, x + w / 2, y + h / 2);
};
/**
* Function: paintVertexShape
*
* Paints the vertex shape.
*/
mxShape.prototype.paintVertexShape = function(c, x, y, w, h)
{
this.paintBackground(c, x, y, w, h);
if (!this.outline || this.style == null || mxUtils.getValue(
this.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0)
{
c.setShadow(false);
this.paintForeground(c, x, y, w, h);
}
};
/**
* Function: paintBackground
*
* Hook for subclassers. This implementation is empty.
*/
mxShape.prototype.paintBackground = function(c, x, y, w, h) { };
/**
* Function: paintForeground
*
* Hook for subclassers. This implementation is empty.
*/
mxShape.prototype.paintForeground = function(c, x, y, w, h) { };
/**
* Function: paintEdgeShape
*
* Hook for subclassers. This implementation is empty.
*/
mxShape.prototype.paintEdgeShape = function(c, pts) { };
/**
* Function: getArcSize
*
* Returns the arc size for the given dimension.
*/
mxShape.prototype.getArcSize = function(w, h)
{
var r = 0;
if (mxUtils.getValue(this.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1')
{
r = Math.min(w / 2, Math.min(h / 2, mxUtils.getValue(this.style,
mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2));
}
else
{
var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE,
mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100;
r = Math.min(w * f, h * f);
}
return r;
};
/**
* Function: paintGlassEffect
*
* Paints the glass gradient effect.
*/
mxShape.prototype.paintGlassEffect = function(c, x, y, w, h, arc)
{
var sw = Math.ceil(this.strokewidth / 2);
var size = 0.4;
c.setGradient('#ffffff', '#ffffff', x, y, w, h * 0.6, 'south', 0.9, 0.1);
c.begin();
arc += 2 * sw;
if (this.isRounded)
{
c.moveTo(x - sw + arc, y - sw);
c.quadTo(x - sw, y - sw, x - sw, y - sw + arc);
c.lineTo(x - sw, y + h * size);
c.quadTo(x + w * 0.5, y + h * 0.7, x + w + sw, y + h * size);
c.lineTo(x + w + sw, y - sw + arc);
c.quadTo(x + w + sw, y - sw, x + w + sw - arc, y - sw);
}
else
{
c.moveTo(x - sw, y - sw);
c.lineTo(x - sw, y + h * size);
c.quadTo(x + w * 0.5, y + h * 0.7, x + w + sw, y + h * size);
c.lineTo(x + w + sw, y - sw);
}
c.close();
c.fill();
};
/**
* Function: addPoints
*
* Paints the given points with rounded corners.
*/
mxShape.prototype.addPoints = function(c, pts, rounded, arcSize, close, exclude, initialMove)
{
if (pts != null && pts.length > 0)
{
initialMove = (initialMove != null) ? initialMove : true;
var pe = pts[pts.length - 1];
// Adds virtual waypoint in the center between start and end point
if (close && rounded)
{
pts = pts.slice();
var p0 = pts[0];
var wp = new mxPoint(pe.x + (p0.x - pe.x) / 2, pe.y + (p0.y - pe.y) / 2);
pts.splice(0, 0, wp);
}
var pt = pts[0];
var i = 1;
// Draws the line segments
if (initialMove)
{
c.moveTo(pt.x, pt.y);
}
else
{
c.lineTo(pt.x, pt.y);
}
while (i < ((close) ? pts.length : pts.length - 1))
{
var tmp = pts[mxUtils.mod(i, pts.length)];
var dx = pt.x - tmp.x;
var dy = pt.y - tmp.y;
if (rounded && (dx != 0 || dy != 0) && (exclude == null || mxUtils.indexOf(exclude, i - 1) < 0))
{
// Draws a line from the last point to the current
// point with a spacing of size off the current point
// into direction of the last point
var dist = Math.sqrt(dx * dx + dy * dy);
var nx1 = dx * Math.min(arcSize, dist / 2) / dist;
var ny1 = dy * Math.min(arcSize, dist / 2) / dist;
var x1 = tmp.x + nx1;
var y1 = tmp.y + ny1;
c.lineTo(x1, y1);
// Draws a curve from the last point to the current
// point with a spacing of size off the current point
// into direction of the next point
var next = pts[mxUtils.mod(i + 1, pts.length)];
// Uses next non-overlapping point
while (i < pts.length - 2 && Math.round(next.x - tmp.x) == 0 && Math.round(next.y - tmp.y) == 0)
{
next = pts[mxUtils.mod(i + 2, pts.length)];
i++;
}
dx = next.x - tmp.x;
dy = next.y - tmp.y;
dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
var nx2 = dx * Math.min(arcSize, dist / 2) / dist;
var ny2 = dy * Math.min(arcSize, dist / 2) / dist;
var x2 = tmp.x + nx2;
var y2 = tmp.y + ny2;
c.quadTo(tmp.x, tmp.y, x2, y2);
tmp = new mxPoint(x2, y2);
}
else
{
c.lineTo(tmp.x, tmp.y);
}
pt = tmp;
i++;
}
if (close)
{
c.close();
}
else
{
c.lineTo(pe.x, pe.y);
}
}
};
/**
* Function: resetStyles
*
* Resets all styles.
*/
mxShape.prototype.resetStyles = function()
{
this.initStyles();
this.spacing = 0;
delete this.fill;
delete this.gradient;
delete this.gradientDirection;
delete this.stroke;
delete this.startSize;
delete this.endSize;
delete this.startArrow;
delete this.endArrow;
delete this.direction;
delete this.isShadow;
delete this.isDashed;
delete this.isRounded;
delete this.glass;
};
/**
* Function: apply
*
* Applies the style of the given <mxCellState> to the shape. This
* implementation assigns the following styles to local fields:
*
* - <mxConstants.STYLE_FILLCOLOR> => fill
* - <mxConstants.STYLE_GRADIENTCOLOR> => gradient
* - <mxConstants.STYLE_GRADIENT_DIRECTION> => gradientDirection
* - <mxConstants.STYLE_OPACITY> => opacity
* - <mxConstants.STYLE_FILL_OPACITY> => fillOpacity
* - <mxConstants.STYLE_STROKE_OPACITY> => strokeOpacity
* - <mxConstants.STYLE_STROKECOLOR> => stroke
* - <mxConstants.STYLE_STROKEWIDTH> => strokewidth
* - <mxConstants.STYLE_SHADOW> => isShadow
* - <mxConstants.STYLE_DASHED> => isDashed
* - <mxConstants.STYLE_SPACING> => spacing
* - <mxConstants.STYLE_STARTSIZE> => startSize
* - <mxConstants.STYLE_ENDSIZE> => endSize
* - <mxConstants.STYLE_ROUNDED> => isRounded
* - <mxConstants.STYLE_STARTARROW> => startArrow
* - <mxConstants.STYLE_ENDARROW> => endArrow
* - <mxConstants.STYLE_ROTATION> => rotation
* - <mxConstants.STYLE_DIRECTION> => direction
* - <mxConstants.STYLE_GLASS> => glass
*
* This keeps a reference to the <style>. If you need to keep a reference to
* the cell, you can override this method and store a local reference to
* state.cell or the <mxCellState> itself. If <outline> should be true, make
* sure to set it before calling this method.
*
* Parameters:
*
* state - <mxCellState> of the corresponding cell.
*/
mxShape.prototype.apply = function(state)
{
this.state = state;
this.style = state.style;
if (this.style != null)
{
this.fill = mxUtils.getValue(this.style, mxConstants.STYLE_FILLCOLOR, this.fill);
this.gradient = mxUtils.getValue(this.style, mxConstants.STYLE_GRADIENTCOLOR, this.gradient);
this.gradientDirection = mxUtils.getValue(this.style, mxConstants.STYLE_GRADIENT_DIRECTION, this.gradientDirection);
this.opacity = mxUtils.getValue(this.style, mxConstants.STYLE_OPACITY, this.opacity);
this.fillOpacity = mxUtils.getValue(this.style, mxConstants.STYLE_FILL_OPACITY, this.fillOpacity);
this.strokeOpacity = mxUtils.getValue(this.style, mxConstants.STYLE_STROKE_OPACITY, this.strokeOpacity);
this.stroke = mxUtils.getValue(this.style, mxConstants.STYLE_STROKECOLOR, this.stroke);
this.strokewidth = mxUtils.getNumber(this.style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth);
this.spacing = mxUtils.getValue(this.style, mxConstants.STYLE_SPACING, this.spacing);
this.startSize = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, this.startSize);
this.endSize = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, this.endSize);
this.startArrow = mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, this.startArrow);
this.endArrow = mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, this.endArrow);
this.rotation = mxUtils.getValue(this.style, mxConstants.STYLE_ROTATION, this.rotation);
this.direction = mxUtils.getValue(this.style, mxConstants.STYLE_DIRECTION, this.direction);
this.flipH = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPH, 0) == 1;
this.flipV = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPV, 0) == 1;
// Legacy support for stencilFlipH/V
if (this.stencil != null)
{
this.flipH = mxUtils.getValue(this.style, 'stencilFlipH', 0) == 1 || this.flipH;
this.flipV = mxUtils.getValue(this.style, 'stencilFlipV', 0) == 1 || this.flipV;
}
if (this.direction == mxConstants.DIRECTION_NORTH || this.direction == mxConstants.DIRECTION_SOUTH)
{
var tmp = this.flipH;
this.flipH = this.flipV;
this.flipV = tmp;
}
this.isShadow = mxUtils.getValue(this.style, mxConstants.STYLE_SHADOW, this.isShadow) == 1;
this.isDashed = mxUtils.getValue(this.style, mxConstants.STYLE_DASHED, this.isDashed) == 1;
this.isRounded = mxUtils.getValue(this.style, mxConstants.STYLE_ROUNDED, this.isRounded) == 1;
this.glass = mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, this.glass) == 1;
if (this.fill == mxConstants.NONE)
{
this.fill = null;
}
if (this.gradient == mxConstants.NONE)
{
this.gradient = null;
}
if (this.stroke == mxConstants.NONE)
{
this.stroke = null;
}
}
};
/**
* Function: setCursor
*
* Sets the cursor on the given shape.
*
* Parameters:
*
* cursor - The cursor to be used.
*/
mxShape.prototype.setCursor = function(cursor)
{
if (cursor == null)
{
cursor = '';
}
this.cursor = cursor;
if (this.node != null)
{
this.node.style.cursor = cursor;
}
};
/**
* Function: getCursor
*
* Returns the current cursor.
*/
mxShape.prototype.getCursor = function()
{
return this.cursor;
};
/**
* Function: isRoundable
*
* Hook for subclassers.
*/
mxShape.prototype.isRoundable = function()
{
return false;
};
/**
* Function: updateBoundingBox
*
* Updates the <boundingBox> for this shape using <createBoundingBox> and
* <augmentBoundingBox> and stores the result in <boundingBox>.
*/
mxShape.prototype.updateBoundingBox = function()
{
// Tries to get bounding box from SVG subsystem
// LATER: Use getBoundingClientRect for fallback in VML
if (this.useSvgBoundingBox && this.node != null && this.node.ownerSVGElement != null)
{
try
{
var b = this.node.getBBox();
if (b.width > 0 && b.height > 0)
{
this.boundingBox = new mxRectangle(b.x, b.y, b.width, b.height);
// Adds strokeWidth
this.boundingBox.grow(this.strokewidth * this.scale / 2);
return;
}
}
catch(e)
{
// fallback to code below
}
}
if (this.bounds != null)
{
var bbox = this.createBoundingBox();
if (bbox != null)
{
this.augmentBoundingBox(bbox);
var rot = this.getShapeRotation();
if (rot != 0)
{
bbox = mxUtils.getBoundingBox(bbox, rot);
}
}
this.boundingBox = bbox;
}
};
/**
* Function: createBoundingBox
*
* Returns a new rectangle that represents the bounding box of the bare shape
* with no shadows or strokewidths.
*/
mxShape.prototype.createBoundingBox = function()
{
var bb = this.bounds.clone();
if ((this.stencil != null && (this.direction == mxConstants.DIRECTION_NORTH ||
this.direction == mxConstants.DIRECTION_SOUTH)) || this.isPaintBoundsInverted())
{
bb.rotate90();
}
return bb;
};
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the strokewidth and shadow offsets.
*/
mxShape.prototype.augmentBoundingBox = function(bbox)
{
if (this.isShadow)
{
bbox.width += Math.ceil(mxConstants.SHADOW_OFFSET_X * this.scale);
bbox.height += Math.ceil(mxConstants.SHADOW_OFFSET_Y * this.scale);
}
// Adds strokeWidth
bbox.grow(this.strokewidth * this.scale / 2);
};
/**
* Function: isPaintBoundsInverted
*
* Returns true if the bounds should be inverted.
*/
mxShape.prototype.isPaintBoundsInverted = function()
{
// Stencil implements inversion via aspect
return this.stencil == null && (this.direction == mxConstants.DIRECTION_NORTH ||
this.direction == mxConstants.DIRECTION_SOUTH);
};
/**
* Function: getRotation
*
* Returns the rotation from the style.
*/
mxShape.prototype.getRotation = function()
{
return (this.rotation != null) ? this.rotation : 0;
};
/**
* Function: getTextRotation
*
* Returns the rotation for the text label.
*/
mxShape.prototype.getTextRotation = function()
{
var rot = this.getRotation();
if (mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, 1) != 1)
{
rot += mxText.prototype.verticalTextRotation;
}
return rot;
};
/**
* Function: getShapeRotation
*
* Returns the actual rotation of the shape.
*/
mxShape.prototype.getShapeRotation = function()
{
var rot = this.getRotation();
if (this.direction != null)
{
if (this.direction == mxConstants.DIRECTION_NORTH)
{
rot += 270;
}
else if (this.direction == mxConstants.DIRECTION_WEST)
{
rot += 180;
}
else if (this.direction == mxConstants.DIRECTION_SOUTH)
{
rot += 90;
}
}
return rot;
};
/**
* Function: createTransparentSvgRectangle
*
* Adds a transparent rectangle that catches all events.
*/
mxShape.prototype.createTransparentSvgRectangle = function(x, y, w, h)
{
var rect = document.createElementNS(mxConstants.NS_SVG, 'rect');
rect.setAttribute('x', x);
rect.setAttribute('y', y);
rect.setAttribute('width', w);
rect.setAttribute('height', h);
rect.setAttribute('fill', 'none');
rect.setAttribute('stroke', 'none');
rect.setAttribute('pointer-events', 'all');
return rect;
};
/**
* Function: setTransparentBackgroundImage
*
* Sets a transparent background CSS style to catch all events.
*
* Paints the line shape.
*/
mxShape.prototype.setTransparentBackgroundImage = function(node)
{
node.style.backgroundImage = 'url(\'' + mxClient.imageBasePath + '/transparent.gif\')';
};
/**
* Function: releaseSvgGradients
*
* Paints the line shape.
*/
mxShape.prototype.releaseSvgGradients = function(grads)
{
if (grads != null)
{
for (var key in grads)
{
var gradient = grads[key];
if (gradient != null)
{
gradient.mxRefCount = (gradient.mxRefCount || 0) - 1;
if (gradient.mxRefCount == 0 && gradient.parentNode != null)
{
gradient.parentNode.removeChild(gradient);
}
}
}
}
};
/**
* Function: destroy
*
* Destroys the shape by removing it from the DOM and releasing the DOM
* node associated with the shape using <mxEvent.release>.
*/
mxShape.prototype.destroy = function()
{
if (this.node != null)
{
mxEvent.release(this.node);
if (this.node.parentNode != null)
{
this.node.parentNode.removeChild(this.node);
}
this.node = null;
}
// Decrements refCount and removes unused
this.releaseSvgGradients(this.oldGradients);
this.oldGradients = null;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStencil
*
* Implements a generic shape which is based on a XML node as a description.
*
* shape:
*
* The outer element is *shape*, that has attributes:
*
* - "name", string, required. The stencil name that uniquely identifies the shape.
* - "w" and "h" are optional decimal view bounds. This defines your co-ordinate
* system for the graphics operations in the shape. The default is 100,100.
* - "aspect", optional string. Either "variable", the default, or "fixed". Fixed
* means always render the shape with the aspect ratio defined by the ratio w/h.
* Variable causes the ratio to match that of the geometry of the current vertex.
* - "strokewidth", optional string. Either an integer or the string "inherit".
* "inherit" indicates that the strokeWidth of the cell is only changed on scaling,
* not on resizing. Default is "1".
* If numeric values are used, the strokeWidth of the cell is changed on both
* scaling and resizing and the value defines the multiple that is applied to
* the width.
*
* connections:
*
* If you want to define specific fixed connection points on the shape use the
* *connections* element. Each *constraint* element within connections defines
* a fixed connection point on the shape. Constraints have attributes:
*
* - "perimeter", required. 1 or 0. 0 sets the connection point where specified
* by x,y. 1 Causes the position of the connection point to be extrapolated from
* the center of the shape, through x,y to the point of intersection with the
* perimeter of the shape.
* - "x" and "y" are the position of the fixed point relative to the bounds of
* the shape. They can be automatically adjusted if perimeter=1. So, (0,0) is top
* left, (0.5,0.5) the center, (1,0.5) the center of the right hand edge of the
* bounds, etc. Values may be less than 0 or greater than 1 to be positioned
* outside of the shape.
* - "name", optional string. A unique identifier for the port on the shape.
*
* background and foreground:
*
* The path of the graphics drawing is split into two elements, *foreground* and
* *background*. The split is to define which part any shadow applied to the shape
* is derived from (the background). This, generally, means the background is the
* line tracing of the outside of the shape, but not always.
*
* Any stroke, fill or fillstroke of a background must be the first element of the
* foreground element, they must not be used within *background*. If the background
* is empty, this is not required.
*
* Because the background cannot have any fill or stroke, it can contain only one
* *path*, *rect*, *roundrect* or *ellipse* element (or none). It can also not
* include *image*, *text* or *include-shape*.
*
* Note that the state, styling and drawing in mxGraph stencils is very close in
* design to that of HTML 5 canvas. Tutorials on this subject, if you're not
* familiar with the topic, will give a good high-level introduction to the
* concepts used.
*
* State:
*
* Rendering within the foreground and background elements has the concept of
* state. There are two types of operations other than state save/load, styling
* and drawing. The styling operations change the current state, so you can save
* the current state with <save/> and pull the last saved state from the state
* stack using <restore/>.
*
* Styling:
*
* The elements that change colors within the current state all take a hash
* prefixed hex color code ("#FFEA80").
*
* - *strokecolor*, this sets the color that drawing paths will be rendered in
* when a stroke or fillstroke command is issued.
* - *fillcolor*, this sets the color that the inside of closed paths will be
* rendered in when a fill or fillstroke command is issued.
* - *fontcolor*, this sets the color that fonts are rendered in when text is drawn.
*
* *alpha* defines the degree of transparency used between 1.0 for fully opaque
* and 0.0 for fully transparent.
*
* *fillalpha* defines the degree of fill transparency used between 1.0 for fully
* opaque and 0.0 for fully transparent.
*
* *strokealpha* defines the degree of stroke transparency used between 1.0 for
* fully opaque and 0.0 for fully transparent.
*
* *strokewidth* defines the integer thickness of drawing elements rendered by
* stroking. Use fixed="1" to apply the value as-is, without scaling.
*
* *dashed* is "1" for dashing enabled and "0" for disabled.
*
* When *dashed* is enabled the current dash pattern, defined by *dashpattern*,
* is used on strokes. dashpattern is a sequence of space separated "on, off"
* lengths that define what distance to paint the stroke for, then what distance
* to paint nothing for, repeat... The default is "3 3". You could define a more
* complex pattern with "5 3 2 6", for example. Generally, it makes sense to have
* an even number of elements in the dashpattern, but that's not required.
*
* *linejoin*, *linecap* and *miterlimit* are best explained by the Mozilla page
* on Canvas styling (about halfway down). The values are all the same except we
* use "flat" for linecap, instead of Canvas' "butt".
*
* For font styling there are.
*
* - *fontsize*, an integer,
* - *fontstyle*, an ORed bit pattern of bold (1), italic (2) and underline (4),
* i.e bold underline is "5".
* - *fontfamily*, is a string defining the typeface to be used.
*
* Drawing:
*
* Most drawing is contained within a *path* element. Again, the graphic
* primitives are very similar to that of HTML 5 canvas.
*
* - *move* to attributes required decimals (x,y).
* - *line* to attributes required decimals (x,y).
* - *quad* to required decimals (x2,y2) via control point required decimals
* (x1,y1).
* - *curve* to required decimals (x3,y3), via control points required decimals
* (x1,y1) and (x2,y2).
* - *arc*, this doesn't follow the HTML Canvas signatures, instead it's a copy
* of the SVG arc command. The SVG specification documentation gives the best
* description of its behaviors. The attributes are named identically, they are
* decimals and all required.
* - *close* ends the current subpath and causes an automatic straight line to
* be drawn from the current point to the initial point of the current subpath.
*
* Complex drawing:
*
* In addition to the graphics primitive operations there are non-primitive
* operations. These provide an easy method to draw some basic shapes.
*
* - *rect*, attributes "x", "y", "w", "h", all required decimals
* - *roundrect*, attributes "x", "y", "w", "h", all required decimals. Also
* "arcsize" an optional decimal attribute defining how large, the corner curves
* are.
* - *ellipse*, attributes "x", "y", "w", "h", all required decimals.
*
* Note that these 3 shapes and all paths must be followed by either a fill,
* stroke, or fillstroke.
*
* Text:
*
* *text* elements have the following attributes.
*
* - "str", the text string to display, required.
* - "x" and "y", the decimal location (x,y) of the text element, required.
* - "align", the horizontal alignment of the text element, either "left",
* "center" or "right". Optional, default is "left".
* - "valign", the vertical alignment of the text element, either "top", "middle"
* or "bottom". Optional, default is "top".
* - "localized", 0 or 1, if 1 then the "str" actually contains a key to use to
* fetch the value out of mxResources. Optional, default is
* <mxStencil.defaultLocalized>.
* - "vertical", 0 or 1, if 1 the label is rendered vertically (rotated by 90
* degrees). Optional, default is 0.
* - "rotation", angle in degrees (0 to 360). The angle to rotate the text by.
* Optional, default is 0.
* - "align-shape", 0 or 1, if 0 ignore the rotation of the shape when setting
* the text rotation. Optional, default is 1.
*
* If <allowEval> is true, then the text content of the this element can define
* a function which is invoked with the shape as the only argument and returns
* the value for the text element (ignored if the str attribute is not null).
*
* Images:
*
* *image* elements can either be external URLs, or data URIs, where supported
* (not in IE 7-). Attributes are:
*
* - "src", required string. Either a data URI or URL.
* - "x", "y", required decimals. The (x,y) position of the image.
* - "w", "h", required decimals. The width and height of the image.
* - "flipH" and "flipV", optional 0 or 1. Whether to flip the image along the
* horizontal/vertical axis. Default is 0 for both.
*
* If <allowEval> is true, then the text content of the this element can define
* a function which is invoked with the shape as the only argument and returns
* the value for the image source (ignored if the src attribute is not null).
*
* Sub-shapes:
*
* *include-shape* allow stencils to be rendered within the current stencil by
* referencing the sub-stencil by name. Attributes are:
*
* - "name", required string. The unique shape name of the stencil.
* - "x", "y", "w", "h", required decimals. The (x,y) position of the sub-shape
* and its width and height.
*
* Constructor: mxStencil
*
* Constructs a new generic shape by setting <desc> to the given XML node and
* invoking <parseDescription> and <parseConstraints>.
*
* Parameters:
*
* desc - XML node that contains the stencil description.
*/
function mxStencil(desc)
{
this.desc = desc;
this.parseDescription();
this.parseConstraints();
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxStencil, mxShape);
/**
* Variable: defaultLocalized
*
* Static global variable that specifies the default value for the localized
* attribute of the text element. Default is false.
*/
mxStencil.defaultLocalized = false;
/**
* Function: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content and images. Default is false. Set this to true
* if stencils can not contain user input.
*/
mxStencil.allowEval = false;
/**
* Variable: desc
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.desc = null;
/**
* Variable: constraints
*
* Holds an array of <mxConnectionConstraints> as defined in the shape.
*/
mxStencil.prototype.constraints = null;
/**
* Variable: aspect
*
* Holds the aspect of the shape. Default is 'auto'.
*/
mxStencil.prototype.aspect = null;
/**
* Variable: w0
*
* Holds the width of the shape. Default is 100.
*/
mxStencil.prototype.w0 = null;
/**
* Variable: h0
*
* Holds the height of the shape. Default is 100.
*/
mxStencil.prototype.h0 = null;
/**
* Variable: bgNodes
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.bgNode = null;
/**
* Variable: fgNodes
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.fgNode = null;
/**
* Variable: strokewidth
*
* Holds the strokewidth direction from the description.
*/
mxStencil.prototype.strokewidth = null;
/**
* Function: parseDescription
*
* Reads <w0>, <h0>, <aspect>, <bgNodes> and <fgNodes> from <desc>.
*/
mxStencil.prototype.parseDescription = function()
{
// LATER: Preprocess nodes for faster painting
this.fgNode = this.desc.getElementsByTagName('foreground')[0];
this.bgNode = this.desc.getElementsByTagName('background')[0];
this.w0 = Number(this.desc.getAttribute('w') || 100);
this.h0 = Number(this.desc.getAttribute('h') || 100);
// Possible values for aspect are: variable and fixed where
// variable means fill the available space and fixed means
// use w0 and h0 to compute the aspect.
var aspect = this.desc.getAttribute('aspect');
this.aspect = (aspect != null) ? aspect : 'variable';
// Possible values for strokewidth are all numbers and "inherit"
// where the inherit means take the value from the style (ie. the
// user-defined stroke-width). Note that the strokewidth is scaled
// by the minimum scaling that is used to draw the shape (sx, sy).
var sw = this.desc.getAttribute('strokewidth');
this.strokewidth = (sw != null) ? sw : '1';
};
/**
* Function: parseConstraints
*
* Reads the constraints from <desc> into <constraints> using
* <parseConstraint>.
*/
mxStencil.prototype.parseConstraints = function()
{
var conns = this.desc.getElementsByTagName('connections')[0];
if (conns != null)
{
var tmp = mxUtils.getChildNodes(conns);
if (tmp != null && tmp.length > 0)
{
this.constraints = [];
for (var i = 0; i < tmp.length; i++)
{
this.constraints.push(this.parseConstraint(tmp[i]));
}
}
}
};
/**
* Function: parseConstraint
*
* Parses the given XML node and returns its <mxConnectionConstraint>.
*/
mxStencil.prototype.parseConstraint = function(node)
{
var x = Number(node.getAttribute('x'));
var y = Number(node.getAttribute('y'));
var perimeter = node.getAttribute('perimeter') == '1';
var name = node.getAttribute('name');
return new mxConnectionConstraint(new mxPoint(x, y), perimeter, name);
};
/**
* Function: evaluateTextAttribute
*
* Gets the given attribute as a text. The return value from <evaluateAttribute>
* is used as a key to <mxResources.get> if the localized attribute in the text
* node is 1 or if <defaultLocalized> is true.
*/
mxStencil.prototype.evaluateTextAttribute = function(node, attribute, shape)
{
var result = this.evaluateAttribute(node, attribute, shape);
var loc = node.getAttribute('localized');
if ((mxStencil.defaultLocalized && loc == null) || loc == '1')
{
result = mxResources.get(result);
}
return result;
};
/**
* Function: evaluateAttribute
*
* Gets the attribute for the given name from the given node. If the attribute
* does not exist then the text content of the node is evaluated and if it is
* a function it is invoked with <shape> as the only argument and the return
* value is used as the attribute value to be returned.
*/
mxStencil.prototype.evaluateAttribute = function(node, attribute, shape)
{
var result = node.getAttribute(attribute);
if (result == null)
{
var text = mxUtils.getTextContent(node);
if (text != null && mxStencil.allowEval)
{
var funct = mxUtils.eval(text);
if (typeof(funct) == 'function')
{
result = funct(shape);
}
}
}
return result;
};
/**
* Function: drawShape
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawShape = function(canvas, shape, x, y, w, h)
{
// TODO: Internal structure (array of special structs?), relative and absolute
// coordinates (eg. note shape, process vs star, actor etc.), text rendering
// and non-proportional scaling, how to implement pluggable edge shapes
// (start, segment, end blocks), pluggable markers, how to implement
// swimlanes (title area) with this API, add icon, horizontal/vertical
// label, indicator for all shapes, rotation
var direction = mxUtils.getValue(shape.style, mxConstants.STYLE_DIRECTION, null);
var aspect = this.computeAspect(shape.style, x, y, w, h, direction);
var minScale = Math.min(aspect.width, aspect.height);
var sw = (this.strokewidth == 'inherit') ?
Number(mxUtils.getNumber(shape.style, mxConstants.STYLE_STROKEWIDTH, 1)) :
Number(this.strokewidth) * minScale;
canvas.setStrokeWidth(sw);
// Draws a transparent rectangle for catching events
if (shape.style != null && mxUtils.getValue(shape.style, mxConstants.STYLE_POINTER_EVENTS, '0') == '1')
{
canvas.setStrokeColor(mxConstants.NONE);
canvas.rect(x, y, w, h);
canvas.stroke();
canvas.setStrokeColor(shape.stroke);
}
this.drawChildren(canvas, shape, x, y, w, h, this.bgNode, aspect, false, true);
this.drawChildren(canvas, shape, x, y, w, h, this.fgNode, aspect, true,
!shape.outline || shape.style == null || mxUtils.getValue(
shape.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0);
};
/**
* Function: drawChildren
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawChildren = function(canvas, shape, x, y, w, h, node, aspect, disableShadow, paint)
{
if (node != null && w > 0 && h > 0)
{
var tmp = node.firstChild;
while (tmp != null)
{
if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT)
{
this.drawNode(canvas, shape, tmp, aspect, disableShadow, paint);
}
tmp = tmp.nextSibling;
}
}
};
/**
* Function: computeAspect
*
* Returns a rectangle that contains the offset in x and y and the horizontal
* and vertical scale in width and height used to draw this shape inside the
* given <mxRectangle>.
*
* Parameters:
*
* shape - <mxShape> to be drawn.
* bounds - <mxRectangle> that should contain the stencil.
* direction - Optional direction of the shape to be darwn.
*/
mxStencil.prototype.computeAspect = function(shape, x, y, w, h, direction)
{
var x0 = x;
var y0 = y;
var sx = w / this.w0;
var sy = h / this.h0;
var inverse = (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH);
if (inverse)
{
sy = w / this.h0;
sx = h / this.w0;
var delta = (w - h) / 2;
x0 += delta;
y0 -= delta;
}
if (this.aspect == 'fixed')
{
sy = Math.min(sx, sy);
sx = sy;
// Centers the shape inside the available space
if (inverse)
{
x0 += (h - this.w0 * sx) / 2;
y0 += (w - this.h0 * sy) / 2;
}
else
{
x0 += (w - this.w0 * sx) / 2;
y0 += (h - this.h0 * sy) / 2;
}
}
return new mxRectangle(x0, y0, sx, sy);
};
/**
* Function: drawNode
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawNode = function(canvas, shape, node, aspect, disableShadow, paint)
{
var name = node.nodeName;
var x0 = aspect.x;
var y0 = aspect.y;
var sx = aspect.width;
var sy = aspect.height;
var minScale = Math.min(sx, sy);
if (name == 'save')
{
canvas.save();
}
else if (name == 'restore')
{
canvas.restore();
}
else if (paint)
{
if (name == 'path')
{
canvas.begin();
var parseRegularly = true;
if (node.getAttribute('rounded') == '1')
{
parseRegularly = false;
var arcSize = Number(node.getAttribute('arcSize'));
var pointCount = 0;
var segs = [];
// Renders the elements inside the given path
var childNode = node.firstChild;
while (childNode != null)
{
if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var childName = childNode.nodeName;
if (childName == 'move' || childName == 'line')
{
if (childName == 'move' || segs.length == 0)
{
segs.push([]);
}
segs[segs.length - 1].push(new mxPoint(x0 + Number(childNode.getAttribute('x')) * sx,
y0 + Number(childNode.getAttribute('y')) * sy));
pointCount++;
}
else
{
//We only support move and line for rounded corners
parseRegularly = true;
break;
}
}
childNode = childNode.nextSibling;
}
if (!parseRegularly && pointCount > 0)
{
for (var i = 0; i < segs.length; i++)
{
var close = false, ps = segs[i][0], pe = segs[i][segs[i].length - 1];
if (ps.x == pe.x && ps.y == pe.y)
{
segs[i].pop();
close = true;
}
this.addPoints(canvas, segs[i], true, arcSize, close);
}
}
else
{
parseRegularly = true;
}
}
if (parseRegularly)
{
// Renders the elements inside the given path
var childNode = node.firstChild;
while (childNode != null)
{
if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT)
{
this.drawNode(canvas, shape, childNode, aspect, disableShadow, paint);
}
childNode = childNode.nextSibling;
}
}
}
else if (name == 'close')
{
canvas.close();
}
else if (name == 'move')
{
canvas.moveTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'line')
{
canvas.lineTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'quad')
{
canvas.quadTo(x0 + Number(node.getAttribute('x1')) * sx,
y0 + Number(node.getAttribute('y1')) * sy,
x0 + Number(node.getAttribute('x2')) * sx,
y0 + Number(node.getAttribute('y2')) * sy);
}
else if (name == 'curve')
{
canvas.curveTo(x0 + Number(node.getAttribute('x1')) * sx,
y0 + Number(node.getAttribute('y1')) * sy,
x0 + Number(node.getAttribute('x2')) * sx,
y0 + Number(node.getAttribute('y2')) * sy,
x0 + Number(node.getAttribute('x3')) * sx,
y0 + Number(node.getAttribute('y3')) * sy);
}
else if (name == 'arc')
{
canvas.arcTo(Number(node.getAttribute('rx')) * sx,
Number(node.getAttribute('ry')) * sy,
Number(node.getAttribute('x-axis-rotation')),
Number(node.getAttribute('large-arc-flag')),
Number(node.getAttribute('sweep-flag')),
x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'rect')
{
canvas.rect(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy);
}
else if (name == 'roundrect')
{
var arcsize = Number(node.getAttribute('arcsize'));
if (arcsize == 0)
{
arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100;
}
var w = Number(node.getAttribute('w')) * sx;
var h = Number(node.getAttribute('h')) * sy;
var factor = Number(arcsize) / 100;
var r = Math.min(w * factor, h * factor);
canvas.roundrect(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
w, h, r, r);
}
else if (name == 'ellipse')
{
canvas.ellipse(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy);
}
else if (name == 'image')
{
if (!shape.outline)
{
var src = this.evaluateAttribute(node, 'src', shape);
canvas.image(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy,
src, false, node.getAttribute('flipH') == '1',
node.getAttribute('flipV') == '1');
}
}
else if (name == 'text')
{
if (!shape.outline)
{
var str = this.evaluateTextAttribute(node, 'str', shape);
var rotation = node.getAttribute('vertical') == '1' ? -90 : 0;
if (node.getAttribute('align-shape') == '0')
{
var dr = shape.rotation;
// Depends on flipping
var flipH = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPH, 0) == 1;
var flipV = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPV, 0) == 1;
if (flipH && flipV)
{
rotation -= dr;
}
else if (flipH || flipV)
{
rotation += dr;
}
else
{
rotation -= dr;
}
}
rotation -= node.getAttribute('rotation');
canvas.text(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
0, 0, str, node.getAttribute('align') || 'left',
node.getAttribute('valign') || 'top', false, '',
null, false, rotation);
}
}
else if (name == 'include-shape')
{
var stencil = mxStencilRegistry.getStencil(node.getAttribute('name'));
if (stencil != null)
{
var x = x0 + Number(node.getAttribute('x')) * sx;
var y = y0 + Number(node.getAttribute('y')) * sy;
var w = Number(node.getAttribute('w')) * sx;
var h = Number(node.getAttribute('h')) * sy;
stencil.drawShape(canvas, shape, x, y, w, h);
}
}
else if (name == 'fillstroke')
{
canvas.fillAndStroke();
}
else if (name == 'fill')
{
canvas.fill();
}
else if (name == 'stroke')
{
canvas.stroke();
}
else if (name == 'strokewidth')
{
var s = (node.getAttribute('fixed') == '1') ? 1 : minScale;
canvas.setStrokeWidth(Number(node.getAttribute('width')) * s);
}
else if (name == 'dashed')
{
canvas.setDashed(node.getAttribute('dashed') == '1');
}
else if (name == 'dashpattern')
{
var value = node.getAttribute('pattern');
if (value != null)
{
var tmp = value.split(' ');
var pat = [];
for (var i = 0; i < tmp.length; i++)
{
if (tmp[i].length > 0)
{
pat.push(Number(tmp[i]) * minScale);
}
}
value = pat.join(' ');
canvas.setDashPattern(value);
}
}
else if (name == 'strokecolor')
{
canvas.setStrokeColor(node.getAttribute('color'));
}
else if (name == 'linecap')
{
canvas.setLineCap(node.getAttribute('cap'));
}
else if (name == 'linejoin')
{
canvas.setLineJoin(node.getAttribute('join'));
}
else if (name == 'miterlimit')
{
canvas.setMiterLimit(Number(node.getAttribute('limit')));
}
else if (name == 'fillcolor')
{
canvas.setFillColor(node.getAttribute('color'));
}
else if (name == 'alpha')
{
canvas.setAlpha(node.getAttribute('alpha'));
}
else if (name == 'fillalpha')
{
canvas.setAlpha(node.getAttribute('alpha'));
}
else if (name == 'strokealpha')
{
canvas.setAlpha(node.getAttribute('alpha'));
}
else if (name == 'fontcolor')
{
canvas.setFontColor(node.getAttribute('color'));
}
else if (name == 'fontstyle')
{
canvas.setFontStyle(node.getAttribute('style'));
}
else if (name == 'fontfamily')
{
canvas.setFontFamily(node.getAttribute('family'));
}
else if (name == 'fontsize')
{
canvas.setFontSize(Number(node.getAttribute('size')) * minScale);
}
if (disableShadow && (name == 'fillstroke' || name == 'fill' || name == 'stroke'))
{
disableShadow = false;
canvas.setShadow(false);
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*
* Code to add stencils.
*
* (code)
* var req = mxUtils.load('test/stencils.xml');
* var root = req.getDocumentElement();
* var shape = root.firstChild;
*
* while (shape != null)
* {
* if (shape.nodeType == mxConstants.NODETYPE_ELEMENT)
* {
* mxStencilRegistry.addStencil(shape.getAttribute('name'), new mxStencil(shape));
* }
*
* shape = shape.nextSibling;
* }
* (end)
*/
var mxStencilRegistry =
{
/**
* Class: mxStencilRegistry
*
* A singleton class that provides a registry for stencils and the methods
* for painting those stencils onto a canvas or into a DOM.
*/
stencils: {},
/**
* Function: addStencil
*
* Adds the given <mxStencil>.
*/
addStencil: function(name, stencil)
{
mxStencilRegistry.stencils[name] = stencil;
},
/**
* Function: getStencil
*
* Returns the <mxStencil> for the given name.
*/
getStencil: function(name)
{
return mxStencilRegistry.stencils[name];
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxMarker =
{
/**
* Class: mxMarker
*
* A static class that implements all markers for VML and SVG using a
* registry. NOTE: The signatures in this class will change.
*
* Variable: markers
*
* Maps from markers names to functions to paint the markers.
*/
markers: [],
/**
* Function: addMarker
*
* Adds a factory method that updates a given endpoint and returns a
* function to paint the marker onto the given canvas.
*/
addMarker: function(type, funct)
{
mxMarker.markers[type] = funct;
},
/**
* Function: createMarker
*
* Returns a function to paint the given marker.
*/
createMarker: function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
{
var funct = mxMarker.markers[type];
return (funct != null) ? funct(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled) : null;
}
};
/**
* Adds the classic and block marker factory method.
*/
(function()
{
function createArrow(widthFactor)
{
widthFactor = (widthFactor != null) ? widthFactor : 2;
return function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
{
// The angle of the forward facing arrow sides against the x axis is
// 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for
// only half the strokewidth is processed ).
var endOffsetX = unitX * sw * 1.118;
var endOffsetY = unitY * sw * 1.118;
unitX = unitX * (size + sw);
unitY = unitY * (size + sw);
var pt = pe.clone();
pt.x -= endOffsetX;
pt.y -= endOffsetY;
var f = (type != mxConstants.ARROW_CLASSIC && type != mxConstants.ARROW_CLASSIC_THIN) ? 1 : 3 / 4;
pe.x += -unitX * f - endOffsetX;
pe.y += -unitY * f - endOffsetY;
return function()
{
canvas.begin();
canvas.moveTo(pt.x, pt.y);
canvas.lineTo(pt.x - unitX - unitY / widthFactor, pt.y - unitY + unitX / widthFactor);
if (type == mxConstants.ARROW_CLASSIC || type == mxConstants.ARROW_CLASSIC_THIN)
{
canvas.lineTo(pt.x - unitX * 3 / 4, pt.y - unitY * 3 / 4);
}
canvas.lineTo(pt.x + unitY / widthFactor - unitX, pt.y - unitY - unitX / widthFactor);
canvas.close();
if (filled)
{
canvas.fillAndStroke();
}
else
{
canvas.stroke();
}
};
}
};
mxMarker.addMarker('classic', createArrow(2));
mxMarker.addMarker('classicThin', createArrow(3));
mxMarker.addMarker('block', createArrow(2));
mxMarker.addMarker('blockThin', createArrow(3));
function createOpenArrow(widthFactor)
{
widthFactor = (widthFactor != null) ? widthFactor : 2;
return function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
{
// The angle of the forward facing arrow sides against the x axis is
// 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for
// only half the strokewidth is processed ).
var endOffsetX = unitX * sw * 1.118;
var endOffsetY = unitY * sw * 1.118;
unitX = unitX * (size + sw);
unitY = unitY * (size + sw);
var pt = pe.clone();
pt.x -= endOffsetX;
pt.y -= endOffsetY;
pe.x += -endOffsetX * 2;
pe.y += -endOffsetY * 2;
return function()
{
canvas.begin();
canvas.moveTo(pt.x - unitX - unitY / widthFactor, pt.y - unitY + unitX / widthFactor);
canvas.lineTo(pt.x, pt.y);
canvas.lineTo(pt.x + unitY / widthFactor - unitX, pt.y - unitY - unitX / widthFactor);
canvas.stroke();
};
}
};
mxMarker.addMarker('open', createOpenArrow(2));
mxMarker.addMarker('openThin', createOpenArrow(3));
mxMarker.addMarker('oval', function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
{
var a = size / 2;
var pt = pe.clone();
pe.x -= unitX * a;
pe.y -= unitY * a;
return function()
{
canvas.ellipse(pt.x - a, pt.y - a, size, size);
if (filled)
{
canvas.fillAndStroke();
}
else
{
canvas.stroke();
}
};
});
function diamond(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
{
// The angle of the forward facing arrow sides against the x axis is
// 45 degrees, 1/sin(45) = 1.4142 / 2 = 0.7071 ( / 2 allows for
// only half the strokewidth is processed ). Or 0.9862 for thin diamond.
// Note these values and the tk variable below are dependent, update
// both together (saves trig hard coding it).
var swFactor = (type == mxConstants.ARROW_DIAMOND) ? 0.7071 : 0.9862;
var endOffsetX = unitX * sw * swFactor;
var endOffsetY = unitY * sw * swFactor;
unitX = unitX * (size + sw);
unitY = unitY * (size + sw);
var pt = pe.clone();
pt.x -= endOffsetX;
pt.y -= endOffsetY;
pe.x += -unitX - endOffsetX;
pe.y += -unitY - endOffsetY;
// thickness factor for diamond
var tk = ((type == mxConstants.ARROW_DIAMOND) ? 2 : 3.4);
return function()
{
canvas.begin();
canvas.moveTo(pt.x, pt.y);
canvas.lineTo(pt.x - unitX / 2 - unitY / tk, pt.y + unitX / tk - unitY / 2);
canvas.lineTo(pt.x - unitX, pt.y - unitY);
canvas.lineTo(pt.x - unitX / 2 + unitY / tk, pt.y - unitY / 2 - unitX / tk);
canvas.close();
if (filled)
{
canvas.fillAndStroke();
}
else
{
canvas.stroke();
}
};
};
mxMarker.addMarker('diamond', diamond);
mxMarker.addMarker('diamondThin', diamond);
})();
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxActor
*
* Extends <mxShape> to implement an actor shape. If a custom shape with one
* filled area is needed, then this shape's <redrawPath> should be overridden.
*
* Example:
*
* (code)
* function SampleShape() { }
*
* SampleShape.prototype = new mxActor();
* SampleShape.prototype.constructor = vsAseShape;
*
* mxCellRenderer.registerShape('sample', SampleShape);
* SampleShape.prototype.redrawPath = function(path, x, y, w, h)
* {
* path.moveTo(0, 0);
* path.lineTo(w, h);
* // ...
* path.close();
* }
* (end)
*
* This shape is registered under <mxConstants.SHAPE_ACTOR> in
* <mxCellRenderer>.
*
* Constructor: mxActor
*
* Constructs a new actor shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxActor(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxActor, mxShape);
/**
* Function: paintVertexShape
*
* Redirects to redrawPath for subclasses to work.
*/
mxActor.prototype.paintVertexShape = function(c, x, y, w, h)
{
c.translate(x, y);
c.begin();
this.redrawPath(c, x, y, w, h);
c.fillAndStroke();
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxActor.prototype.redrawPath = function(c, x, y, w, h)
{
var width = w/3;
c.moveTo(0, h);
c.curveTo(0, 3 * h / 5, 0, 2 * h / 5, w / 2, 2 * h / 5);
c.curveTo(w / 2 - width, 2 * h / 5, w / 2 - width, 0, w / 2, 0);
c.curveTo(w / 2 + width, 0, w / 2 + width, 2 * h / 5, w / 2, 2 * h / 5);
c.curveTo(w, 2 * h / 5, w, 3 * h / 5, w, h);
c.close();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCloud
*
* Extends <mxActor> to implement a cloud shape.
*
* This shape is registered under <mxConstants.SHAPE_CLOUD> in
* <mxCellRenderer>.
*
* Constructor: mxCloud
*
* Constructs a new cloud shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxCloud(bounds, fill, stroke, strokewidth)
{
mxActor.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxActor.
*/
mxUtils.extend(mxCloud, mxActor);
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxCloud.prototype.redrawPath = function(c, x, y, w, h)
{
c.moveTo(0.25 * w, 0.25 * h);
c.curveTo(0.05 * w, 0.25 * h, 0, 0.5 * h, 0.16 * w, 0.55 * h);
c.curveTo(0, 0.66 * h, 0.18 * w, 0.9 * h, 0.31 * w, 0.8 * h);
c.curveTo(0.4 * w, h, 0.7 * w, h, 0.8 * w, 0.8 * h);
c.curveTo(w, 0.8 * h, w, 0.6 * h, 0.875 * w, 0.5 * h);
c.curveTo(w, 0.3 * h, 0.8 * w, 0.1 * h, 0.625 * w, 0.2 * h);
c.curveTo(0.5 * w, 0.05 * h, 0.3 * w, 0.05 * h, 0.25 * w, 0.25 * h);
c.close();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxRectangleShape
*
* Extends <mxShape> to implement a rectangle shape.
* This shape is registered under <mxConstants.SHAPE_RECTANGLE>
* in <mxCellRenderer>.
*
* Constructor: mxRectangleShape
*
* Constructs a new rectangle shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxRectangleShape(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxRectangleShape, mxShape);
/**
* Function: isHtmlAllowed
*
* Returns true for non-rounded, non-rotated shapes with no glass gradient.
*/
mxRectangleShape.prototype.isHtmlAllowed = function()
{
var events = true;
if (this.style != null)
{
events = mxUtils.getValue(this.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1';
}
return !this.isRounded && !this.glass && this.rotation == 0 && (events ||
(this.fill != null && this.fill != mxConstants.NONE));
};
/**
* Function: paintBackground
*
* Generic background painting implementation.
*/
mxRectangleShape.prototype.paintBackground = function(c, x, y, w, h)
{
var events = true;
if (this.style != null)
{
events = mxUtils.getValue(this.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1';
}
if (events || (this.fill != null && this.fill != mxConstants.NONE) ||
(this.stroke != null && this.stroke != mxConstants.NONE))
{
if (!events && (this.fill == null || this.fill == mxConstants.NONE))
{
c.pointerEvents = false;
}
if (this.isRounded)
{
var r = 0;
if (mxUtils.getValue(this.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1')
{
r = Math.min(w / 2, Math.min(h / 2, mxUtils.getValue(this.style,
mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2));
}
else
{
var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE,
mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100;
r = Math.min(w * f, h * f);
}
c.roundrect(x, y, w, h, r, r);
}
else
{
c.rect(x, y, w, h);
}
c.fillAndStroke();
}
};
/**
* Function: isRoundable
*
* Adds roundable support.
*/
mxRectangleShape.prototype.isRoundable = function(c, x, y, w, h)
{
return true;
};
/**
* Function: paintForeground
*
* Generic background painting implementation.
*/
mxRectangleShape.prototype.paintForeground = function(c, x, y, w, h)
{
if (this.glass && !this.outline && this.fill != null && this.fill != mxConstants.NONE)
{
this.paintGlassEffect(c, x, y, w, h, this.getArcSize(w + this.strokewidth, h + this.strokewidth));
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEllipse
*
* Extends <mxShape> to implement an ellipse shape.
* This shape is registered under <mxConstants.SHAPE_ELLIPSE>
* in <mxCellRenderer>.
*
* Constructor: mxEllipse
*
* Constructs a new ellipse shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxEllipse(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxEllipse, mxShape);
/**
* Function: paintVertexShape
*
* Paints the ellipse shape.
*/
mxEllipse.prototype.paintVertexShape = function(c, x, y, w, h)
{
c.ellipse(x, y, w, h);
c.fillAndStroke();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDoubleEllipse
*
* Extends <mxShape> to implement a double ellipse shape. This shape is
* registered under <mxConstants.SHAPE_DOUBLE_ELLIPSE> in <mxCellRenderer>.
* Use the following override to only fill the inner ellipse in this shape:
*
* (code)
* mxDoubleEllipse.prototype.paintVertexShape = function(c, x, y, w, h)
* {
* c.ellipse(x, y, w, h);
* c.stroke();
*
* var inset = mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth, Math.min(w / 5, h / 5)));
* x += inset;
* y += inset;
* w -= 2 * inset;
* h -= 2 * inset;
*
* if (w > 0 && h > 0)
* {
* c.ellipse(x, y, w, h);
* }
*
* c.fillAndStroke();
* };
* (end)
*
* Constructor: mxDoubleEllipse
*
* Constructs a new ellipse shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxDoubleEllipse(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxDoubleEllipse, mxShape);
/**
* Variable: vmlScale
*
* Scale for improving the precision of VML rendering. Default is 10.
*/
mxDoubleEllipse.prototype.vmlScale = 10;
/**
* Function: paintBackground
*
* Paints the background.
*/
mxDoubleEllipse.prototype.paintBackground = function(c, x, y, w, h)
{
c.ellipse(x, y, w, h);
c.fillAndStroke();
};
/**
* Function: paintForeground
*
* Paints the foreground.
*/
mxDoubleEllipse.prototype.paintForeground = function(c, x, y, w, h)
{
if (!this.outline)
{
var margin = mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth, Math.min(w / 5, h / 5)));
x += margin;
y += margin;
w -= 2 * margin;
h -= 2 * margin;
// FIXME: Rounding issues in IE8 standards mode (not in 1.x)
if (w > 0 && h > 0)
{
c.ellipse(x, y, w, h);
}
c.stroke();
}
};
/**
* Function: getLabelBounds
*
* Returns the bounds for the label.
*/
mxDoubleEllipse.prototype.getLabelBounds = function(rect)
{
var margin = (mxUtils.getValue(this.style, mxConstants.STYLE_MARGIN, Math.min(3 + this.strokewidth,
Math.min(rect.width / 5 / this.scale, rect.height / 5 / this.scale)))) * this.scale;
return new mxRectangle(rect.x + margin, rect.y + margin, rect.width - 2 * margin, rect.height - 2 * margin);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxRhombus
*
* Extends <mxShape> to implement a rhombus (aka diamond) shape.
* This shape is registered under <mxConstants.SHAPE_RHOMBUS>
* in <mxCellRenderer>.
*
* Constructor: mxRhombus
*
* Constructs a new rhombus shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxRhombus(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxRhombus, mxShape);
/**
* Function: isRoundable
*
* Adds roundable support.
*/
mxRhombus.prototype.isRoundable = function()
{
return true;
};
/**
* Function: paintVertexShape
*
* Generic painting implementation.
*/
mxRhombus.prototype.paintVertexShape = function(c, x, y, w, h)
{
var hw = w / 2;
var hh = h / 2;
var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2;
c.begin();
this.addPoints(c, [new mxPoint(x + hw, y), new mxPoint(x + w, y + hh), new mxPoint(x + hw, y + h),
new mxPoint(x, y + hh)], this.isRounded, arcSize, true);
c.fillAndStroke();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPolyline
*
* Extends <mxShape> to implement a polyline (a line with multiple points).
* This shape is registered under <mxConstants.SHAPE_POLYLINE> in
* <mxCellRenderer>.
*
* Constructor: mxPolyline
*
* Constructs a new polyline shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* stroke - String that defines the stroke color. Default is 'black'. This is
* stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxPolyline(points, stroke, strokewidth)
{
mxShape.call(this);
this.points = points;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxPolyline, mxShape);
/**
* Function: getRotation
*
* Returns 0.
*/
mxPolyline.prototype.getRotation = function()
{
return 0;
};
/**
* Function: getShapeRotation
*
* Returns 0.
*/
mxPolyline.prototype.getShapeRotation = function()
{
return 0;
};
/**
* Function: isPaintBoundsInverted
*
* Returns false.
*/
mxPolyline.prototype.isPaintBoundsInverted = function()
{
return false;
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxPolyline.prototype.paintEdgeShape = function(c, pts)
{
var prev = c.pointerEventsValue;
c.pointerEventsValue = 'stroke';
if (this.style == null || this.style[mxConstants.STYLE_CURVED] != 1)
{
this.paintLine(c, pts, this.isRounded);
}
else
{
this.paintCurvedLine(c, pts);
}
c.pointerEventsValue = prev;
};
/**
* Function: paintLine
*
* Paints the line shape.
*/
mxPolyline.prototype.paintLine = function(c, pts, rounded)
{
var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2;
c.begin();
this.addPoints(c, pts, rounded, arcSize, false);
c.stroke();
};
/**
* Function: paintLine
*
* Paints the line shape.
*/
mxPolyline.prototype.paintCurvedLine = function(c, pts)
{
c.begin();
var pt = pts[0];
var n = pts.length;
c.moveTo(pt.x, pt.y);
for (var i = 1; i < n - 2; i++)
{
var p0 = pts[i];
var p1 = pts[i + 1];
var ix = (p0.x + p1.x) / 2;
var iy = (p0.y + p1.y) / 2;
c.quadTo(p0.x, p0.y, ix, iy);
}
var p0 = pts[n - 2];
var p1 = pts[n - 1];
c.quadTo(p0.x, p0.y, p1.x, p1.y);
c.stroke();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxArrow
*
* Extends <mxShape> to implement an arrow shape. (The shape
* is used to represent edges, not vertices.)
* This shape is registered under <mxConstants.SHAPE_ARROW>
* in <mxCellRenderer>.
*
* Constructor: mxArrow
*
* Constructs a new arrow shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
* arrowWidth - Optional integer that defines the arrow width. Default is
* <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
* spacing - Optional integer that defines the spacing between the arrow shape
* and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
* <spacing>.
* endSize - Optional integer that defines the size of the arrowhead. Default
* is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
*/
function mxArrow(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize)
{
mxShape.call(this);
this.points = points;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH;
this.spacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING;
this.endSize = (endSize != null) ? endSize : mxConstants.ARROW_SIZE;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxArrow, mxShape);
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the edge width and markers.
*/
mxArrow.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
var w = Math.max(this.arrowWidth, this.endSize);
bbox.grow((w / 2 + this.strokewidth) * this.scale);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxArrow.prototype.paintEdgeShape = function(c, pts)
{
// Geometry of arrow
var spacing = mxConstants.ARROW_SPACING;
var width = mxConstants.ARROW_WIDTH;
var arrow = mxConstants.ARROW_SIZE;
// Base vector (between end points)
var p0 = pts[0];
var pe = pts[pts.length - 1];
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var length = dist - 2 * spacing - arrow;
// Computes the norm and the inverse norm
var nx = dx / dist;
var ny = dy / dist;
var basex = length * nx;
var basey = length * ny;
var floorx = width * ny/3;
var floory = -width * nx/3;
// Computes points
var p0x = p0.x - floorx / 2 + spacing * nx;
var p0y = p0.y - floory / 2 + spacing * ny;
var p1x = p0x + floorx;
var p1y = p0y + floory;
var p2x = p1x + basex;
var p2y = p1y + basey;
var p3x = p2x + floorx;
var p3y = p2y + floory;
// p4 not necessary
var p5x = p3x - 3 * floorx;
var p5y = p3y - 3 * floory;
c.begin();
c.moveTo(p0x, p0y);
c.lineTo(p1x, p1y);
c.lineTo(p2x, p2y);
c.lineTo(p3x, p3y);
c.lineTo(pe.x - spacing * nx, pe.y - spacing * ny);
c.lineTo(p5x, p5y);
c.lineTo(p5x + floorx, p5y + floory);
c.close();
c.fillAndStroke();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxArrowConnector
*
* Extends <mxShape> to implement an new rounded arrow shape with support for
* waypoints and double arrows. (The shape is used to represent edges, not
* vertices.) This shape is registered under <mxConstants.SHAPE_ARROW_CONNECTOR>
* in <mxCellRenderer>.
*
* Constructor: mxArrowConnector
*
* Constructs a new arrow shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
* arrowWidth - Optional integer that defines the arrow width. Default is
* <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
* spacing - Optional integer that defines the spacing between the arrow shape
* and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
* <spacing>.
* endSize - Optional integer that defines the size of the arrowhead. Default
* is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
*/
function mxArrowConnector(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize)
{
mxShape.call(this);
this.points = points;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH;
this.arrowSpacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING;
this.startSize = mxConstants.ARROW_SIZE / 5;
this.endSize = mxConstants.ARROW_SIZE / 5;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxArrowConnector, mxShape);
/**
* Variable: useSvgBoundingBox
*
* Allows to use the SVG bounding box in SVG. Default is false for performance
* reasons.
*/
mxArrowConnector.prototype.useSvgBoundingBox = true;
/**
* Variable: resetStyles
*
* Overrides mxShape to reset spacing.
*/
mxArrowConnector.prototype.resetStyles = function()
{
mxShape.prototype.resetStyles.apply(this, arguments);
this.arrowSpacing = mxConstants.ARROW_SPACING;
};
/**
* Overrides apply to get smooth transition from default start- and endsize.
*/
mxArrowConnector.prototype.apply = function(state)
{
mxShape.prototype.apply.apply(this, arguments);
if (this.style != null)
{
this.startSize = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.ARROW_SIZE / 5) * 3;
this.endSize = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.ARROW_SIZE / 5) * 3;
}
};
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the edge width and markers.
*/
mxArrowConnector.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
var w = this.getEdgeWidth();
if (this.isMarkerStart())
{
w = Math.max(w, this.getStartArrowWidth());
}
if (this.isMarkerEnd())
{
w = Math.max(w, this.getEndArrowWidth());
}
bbox.grow((w / 2 + this.strokewidth) * this.scale);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxArrowConnector.prototype.paintEdgeShape = function(c, pts)
{
// Geometry of arrow
var strokeWidth = this.strokewidth;
if (this.outline)
{
strokeWidth = Math.max(1, mxUtils.getNumber(this.style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth));
}
var startWidth = this.getStartArrowWidth() + strokeWidth;
var endWidth = this.getEndArrowWidth() + strokeWidth;
var edgeWidth = this.outline ? this.getEdgeWidth() + strokeWidth : this.getEdgeWidth();
var openEnded = this.isOpenEnded();
var markerStart = this.isMarkerStart();
var markerEnd = this.isMarkerEnd();
var spacing = (openEnded) ? 0 : this.arrowSpacing + strokeWidth / 2;
var startSize = this.startSize + strokeWidth;
var endSize = this.endSize + strokeWidth;
var isRounded = this.isArrowRounded();
// Base vector (between first points)
var pe = pts[pts.length - 1];
// Finds first non-overlapping point
var i0 = 1;
while (i0 < pts.length - 1 && pts[i0].x == pts[0].x && pts[i0].y == pts[0].y)
{
i0++;
}
var dx = pts[i0].x - pts[0].x;
var dy = pts[i0].y - pts[0].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist == 0)
{
return;
}
// Computes the norm and the inverse norm
var nx = dx / dist;
var nx2, nx1 = nx;
var ny = dy / dist;
var ny2, ny1 = ny;
var orthx = edgeWidth * ny;
var orthy = -edgeWidth * nx;
// Stores the inbound function calls in reverse order in fns
var fns = [];
if (isRounded)
{
c.setLineJoin('round');
}
else if (pts.length > 2)
{
// Only mitre if there are waypoints
c.setMiterLimit(1.42);
}
c.begin();
var startNx = nx;
var startNy = ny;
if (markerStart && !openEnded)
{
this.paintMarker(c, pts[0].x, pts[0].y, nx, ny, startSize, startWidth, edgeWidth, spacing, true);
}
else
{
var outStartX = pts[0].x + orthx / 2 + spacing * nx;
var outStartY = pts[0].y + orthy / 2 + spacing * ny;
var inEndX = pts[0].x - orthx / 2 + spacing * nx;
var inEndY = pts[0].y - orthy / 2 + spacing * ny;
if (openEnded)
{
c.moveTo(outStartX, outStartY);
fns.push(function()
{
c.lineTo(inEndX, inEndY);
});
}
else
{
c.moveTo(inEndX, inEndY);
c.lineTo(outStartX, outStartY);
}
}
var dx1 = 0;
var dy1 = 0;
var dist1 = 0;
for (var i = 0; i < pts.length - 2; i++)
{
// Work out in which direction the line is bending
var pos = mxUtils.relativeCcw(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y, pts[i+2].x, pts[i+2].y);
dx1 = pts[i+2].x - pts[i+1].x;
dy1 = pts[i+2].y - pts[i+1].y;
dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
if (dist1 != 0)
{
nx1 = dx1 / dist1;
ny1 = dy1 / dist1;
var tmp1 = nx * nx1 + ny * ny1;
var tmp = Math.max(Math.sqrt((tmp1 + 1) / 2), 0.04);
// Work out the normal orthogonal to the line through the control point and the edge sides intersection
nx2 = (nx + nx1);
ny2 = (ny + ny1);
var dist2 = Math.sqrt(nx2 * nx2 + ny2 * ny2);
if (dist2 != 0)
{
nx2 = nx2 / dist2;
ny2 = ny2 / dist2;
// Higher strokewidths require a larger minimum bend, 0.35 covers all but the most extreme cases
var strokeWidthFactor = Math.max(tmp, Math.min(this.strokewidth / 200 + 0.04, 0.35));
var angleFactor = (pos != 0 && isRounded) ? Math.max(0.1, strokeWidthFactor) : Math.max(tmp, 0.06);
var outX = pts[i+1].x + ny2 * edgeWidth / 2 / angleFactor;
var outY = pts[i+1].y - nx2 * edgeWidth / 2 / angleFactor;
var inX = pts[i+1].x - ny2 * edgeWidth / 2 / angleFactor;
var inY = pts[i+1].y + nx2 * edgeWidth / 2 / angleFactor;
if (pos == 0 || !isRounded)
{
// If the two segments are aligned, or if we're not drawing curved sections between segments
// just draw straight to the intersection point
c.lineTo(outX, outY);
(function(x, y)
{
fns.push(function()
{
c.lineTo(x, y);
});
})(inX, inY);
}
else if (pos == -1)
{
var c1x = inX + ny * edgeWidth;
var c1y = inY - nx * edgeWidth;
var c2x = inX + ny1 * edgeWidth;
var c2y = inY - nx1 * edgeWidth;
c.lineTo(c1x, c1y);
c.quadTo(outX, outY, c2x, c2y);
(function(x, y)
{
fns.push(function()
{
c.lineTo(x, y);
});
})(inX, inY);
}
else
{
c.lineTo(outX, outY);
(function(x, y)
{
var c1x = outX - ny * edgeWidth;
var c1y = outY + nx * edgeWidth;
var c2x = outX - ny1 * edgeWidth;
var c2y = outY + nx1 * edgeWidth;
fns.push(function()
{
c.quadTo(x, y, c1x, c1y);
});
fns.push(function()
{
c.lineTo(c2x, c2y);
});
})(inX, inY);
}
nx = nx1;
ny = ny1;
}
}
}
orthx = edgeWidth * ny1;
orthy = - edgeWidth * nx1;
if (markerEnd && !openEnded)
{
this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, false);
}
else
{
c.lineTo(pe.x - spacing * nx1 + orthx / 2, pe.y - spacing * ny1 + orthy / 2);
var inStartX = pe.x - spacing * nx1 - orthx / 2;
var inStartY = pe.y - spacing * ny1 - orthy / 2;
if (!openEnded)
{
c.lineTo(inStartX, inStartY);
}
else
{
c.moveTo(inStartX, inStartY);
fns.splice(0, 0, function()
{
c.moveTo(inStartX, inStartY);
});
}
}
for (var i = fns.length - 1; i >= 0; i--)
{
fns[i]();
}
if (openEnded)
{
c.end();
c.stroke();
}
else
{
c.close();
c.fillAndStroke();
}
// Workaround for shadow on top of base arrow
c.setShadow(false);
// Need to redraw the markers without the low miter limit
c.setMiterLimit(4);
if (isRounded)
{
c.setLineJoin('flat');
}
if (pts.length > 2)
{
// Only to repaint markers if no waypoints
// Need to redraw the markers without the low miter limit
c.setMiterLimit(4);
if (markerStart && !openEnded)
{
c.begin();
this.paintMarker(c, pts[0].x, pts[0].y, startNx, startNy, startSize, startWidth, edgeWidth, spacing, true);
c.stroke();
c.end();
}
if (markerEnd && !openEnded)
{
c.begin();
this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, true);
c.stroke();
c.end();
}
}
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxArrowConnector.prototype.paintMarker = function(c, ptX, ptY, nx, ny, size, arrowWidth, edgeWidth, spacing, initialMove)
{
var widthArrowRatio = edgeWidth / arrowWidth;
var orthx = edgeWidth * ny / 2;
var orthy = -edgeWidth * nx / 2;
var spaceX = (spacing + size) * nx;
var spaceY = (spacing + size) * ny;
if (initialMove)
{
c.moveTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
}
else
{
c.lineTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
}
c.lineTo(ptX - orthx / widthArrowRatio + spaceX, ptY - orthy / widthArrowRatio + spaceY);
c.lineTo(ptX + spacing * nx, ptY + spacing * ny);
c.lineTo(ptX + orthx / widthArrowRatio + spaceX, ptY + orthy / widthArrowRatio + spaceY);
c.lineTo(ptX + orthx + spaceX, ptY + orthy + spaceY);
}
/**
* Function: isArrowRounded
*
* Returns wether the arrow is rounded
*/
mxArrowConnector.prototype.isArrowRounded = function()
{
return this.isRounded;
};
/**
* Function: getStartArrowWidth
*
* Returns the width of the start arrow
*/
mxArrowConnector.prototype.getStartArrowWidth = function()
{
return mxConstants.ARROW_WIDTH;
};
/**
* Function: getEndArrowWidth
*
* Returns the width of the end arrow
*/
mxArrowConnector.prototype.getEndArrowWidth = function()
{
return mxConstants.ARROW_WIDTH;
};
/**
* Function: getEdgeWidth
*
* Returns the width of the body of the edge
*/
mxArrowConnector.prototype.getEdgeWidth = function()
{
return mxConstants.ARROW_WIDTH / 3;
};
/**
* Function: isOpenEnded
*
* Returns whether the ends of the shape are drawn
*/
mxArrowConnector.prototype.isOpenEnded = function()
{
return false;
};
/**
* Function: isMarkerStart
*
* Returns whether the start marker is drawn
*/
mxArrowConnector.prototype.isMarkerStart = function()
{
return (mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE);
};
/**
* Function: isMarkerEnd
*
* Returns whether the end marker is drawn
*/
mxArrowConnector.prototype.isMarkerEnd = function()
{
return (mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxText
*
* Extends <mxShape> to implement a text shape. To change vertical text from
* bottom to top to top to bottom, the following code can be used:
*
* (code)
* mxText.prototype.verticalTextRotation = 90;
* (end)
*
* Constructor: mxText
*
* Constructs a new text shape.
*
* Parameters:
*
* value - String that represents the text to be displayed. This is stored in
* <value>.
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* align - Specifies the horizontal alignment. Default is ''. This is stored in
* <align>.
* valign - Specifies the vertical alignment. Default is ''. This is stored in
* <valign>.
* color - String that specifies the text color. Default is 'black'. This is
* stored in <color>.
* family - String that specifies the font family. Default is
* <mxConstants.DEFAULT_FONTFAMILY>. This is stored in <family>.
* size - Integer that specifies the font size. Default is
* <mxConstants.DEFAULT_FONTSIZE>. This is stored in <size>.
* fontStyle - Specifies the font style. Default is 0. This is stored in
* <fontStyle>.
* spacing - Integer that specifies the global spacing. Default is 2. This is
* stored in <spacing>.
* spacingTop - Integer that specifies the top spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingTop>.
* spacingRight - Integer that specifies the right spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingRight>.
* spacingBottom - Integer that specifies the bottom spacing. Default is 0.The
* sum of the spacing and this is stored in <spacingBottom>.
* spacingLeft - Integer that specifies the left spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingLeft>.
* horizontal - Boolean that specifies if the label is horizontal. Default is
* true. This is stored in <horizontal>.
* background - String that specifies the background color. Default is null.
* This is stored in <background>.
* border - String that specifies the label border color. Default is null.
* This is stored in <border>.
* wrap - Specifies if word-wrapping should be enabled. Default is false.
* This is stored in <wrap>.
* clipped - Specifies if the label should be clipped. Default is false.
* This is stored in <clipped>.
* overflow - Value of the overflow style. Default is 'visible'.
*/
function mxText(value, bounds, align, valign, color,
family, size, fontStyle, spacing, spacingTop, spacingRight,
spacingBottom, spacingLeft, horizontal, background, border,
wrap, clipped, overflow, labelPadding, textDirection)
{
mxShape.call(this);
this.value = value;
this.bounds = bounds;
this.color = (color != null) ? color : 'black';
this.align = (align != null) ? align : mxConstants.ALIGN_CENTER;
this.valign = (valign != null) ? valign : mxConstants.ALIGN_MIDDLE;
this.family = (family != null) ? family : mxConstants.DEFAULT_FONTFAMILY;
this.size = (size != null) ? size : mxConstants.DEFAULT_FONTSIZE;
this.fontStyle = (fontStyle != null) ? fontStyle : mxConstants.DEFAULT_FONTSTYLE;
this.spacing = parseInt(spacing || 2);
this.spacingTop = this.spacing + parseInt(spacingTop || 0);
this.spacingRight = this.spacing + parseInt(spacingRight || 0);
this.spacingBottom = this.spacing + parseInt(spacingBottom || 0);
this.spacingLeft = this.spacing + parseInt(spacingLeft || 0);
this.horizontal = (horizontal != null) ? horizontal : true;
this.background = background;
this.border = border;
this.wrap = (wrap != null) ? wrap : false;
this.clipped = (clipped != null) ? clipped : false;
this.overflow = (overflow != null) ? overflow : 'visible';
this.labelPadding = (labelPadding != null) ? labelPadding : 0;
this.textDirection = textDirection;
this.rotation = 0;
this.updateMargin();
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxText, mxShape);
/**
* Variable: baseSpacingTop
*
* Specifies the spacing to be added to the top spacing. Default is 0. Use the
* value 5 here to get the same label positions as in mxGraph 1.x.
*/
mxText.prototype.baseSpacingTop = 0;
/**
* Variable: baseSpacingBottom
*
* Specifies the spacing to be added to the bottom spacing. Default is 0. Use the
* value 1 here to get the same label positions as in mxGraph 1.x.
*/
mxText.prototype.baseSpacingBottom = 0;
/**
* Variable: baseSpacingLeft
*
* Specifies the spacing to be added to the left spacing. Default is 0.
*/
mxText.prototype.baseSpacingLeft = 0;
/**
* Variable: baseSpacingRight
*
* Specifies the spacing to be added to the right spacing. Default is 0.
*/
mxText.prototype.baseSpacingRight = 0;
/**
* Variable: replaceLinefeeds
*
* Specifies if linefeeds in HTML labels should be replaced with BR tags.
* Default is true.
*/
mxText.prototype.replaceLinefeeds = true;
/**
* Variable: verticalTextRotation
*
* Rotation for vertical text. Default is -90 (bottom to top).
*/
mxText.prototype.verticalTextRotation = -90;
/**
* Variable: ignoreClippedStringSize
*
* Specifies if the string size should be measured in <updateBoundingBox> if
* the label is clipped and the label position is center and middle. If this is
* true, then the bounding box will be set to <bounds>. Default is true.
* <ignoreStringSize> has precedence over this switch.
*/
mxText.prototype.ignoreClippedStringSize = true;
/**
* Variable: ignoreStringSize
*
* Specifies if the actual string size should be measured. If disabled the
* boundingBox will not ignore the actual size of the string, otherwise
* <bounds> will be used instead. Default is false.
*/
mxText.prototype.ignoreStringSize = false;
/**
* Variable: textWidthPadding
*
* Specifies the padding to be added to the text width for the bounding box.
* This is needed to make sure no clipping is applied to borders. Default is 4
* for IE 8 standards mode and 3 for all others.
*/
mxText.prototype.textWidthPadding = (document.documentMode == 8 && !mxClient.IS_EM) ? 4 : 3;
/**
* Variable: lastValue
*
* Contains the last rendered text value. Used for caching.
*/
mxText.prototype.lastValue = null;
/**
* Variable: cacheEnabled
*
* Specifies if caching for HTML labels should be enabled. Default is true.
*/
mxText.prototype.cacheEnabled = true;
/**
* Function: isParseVml
*
* Text shapes do not contain VML markup and do not need to be parsed. This
* method returns false to speed up rendering in IE8.
*/
mxText.prototype.isParseVml = function()
{
return false;
};
/**
* Function: isHtmlAllowed
*
* Returns true if HTML is allowed for this shape. This implementation returns
* true if the browser is not in IE8 standards mode.
*/
mxText.prototype.isHtmlAllowed = function()
{
return document.documentMode != 8 || mxClient.IS_EM;
};
/**
* Function: getSvgScreenOffset
*
* Disables offset in IE9 for crisper image output.
*/
mxText.prototype.getSvgScreenOffset = function()
{
return 0;
};
/**
* Function: checkBounds
*
* Returns true if the bounds are not null and all of its variables are numeric.
*/
mxText.prototype.checkBounds = function()
{
return (!isNaN(this.scale) && isFinite(this.scale) && this.scale > 0 &&
this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) &&
!isNaN(this.bounds.width) && !isNaN(this.bounds.height));
};
/**
* Function: paint
*
* Generic rendering code.
*/
mxText.prototype.paint = function(c, update)
{
// Scale is passed-through to canvas
var s = this.scale;
var x = this.bounds.x / s;
var y = this.bounds.y / s;
var w = this.bounds.width / s;
var h = this.bounds.height / s;
this.updateTransform(c, x, y, w, h);
this.configureCanvas(c, x, y, w, h);
var unscaledWidth = (this.state != null) ? this.state.unscaledWidth : null;
if (update)
{
if (this.node.firstChild != null && (unscaledWidth == null ||
this.lastUnscaledWidth != unscaledWidth))
{
c.invalidateCachedOffsetSize(this.node);
}
c.updateText(x, y, w, h, this.align, this.valign, this.wrap, this.overflow,
this.clipped, this.getTextRotation(), this.node);
}
else
{
// Checks if text contains HTML markup
var realHtml = mxUtils.isNode(this.value) || this.dialect == mxConstants.DIALECT_STRICTHTML;
// Always renders labels as HTML in VML
var fmt = (realHtml || c instanceof mxVmlCanvas2D) ? 'html' : '';
var val = this.value;
if (!realHtml && fmt == 'html')
{
val = mxUtils.htmlEntities(val, false);
}
if (fmt == 'html' && !mxUtils.isNode(this.value))
{
val = mxUtils.replaceTrailingNewlines(val, '<div><br></div>');
}
// Handles trailing newlines to make sure they are visible in rendering output
val = (!mxUtils.isNode(this.value) && this.replaceLinefeeds && fmt == 'html') ?
val.replace(/\n/g, '<br/>') : val;
var dir = this.textDirection;
if (dir == mxConstants.TEXT_DIRECTION_AUTO && !realHtml)
{
dir = this.getAutoDirection();
}
if (dir != mxConstants.TEXT_DIRECTION_LTR && dir != mxConstants.TEXT_DIRECTION_RTL)
{
dir = null;
}
c.text(x, y, w, h, val, this.align, this.valign, this.wrap, fmt, this.overflow,
this.clipped, this.getTextRotation(), dir);
}
// Needs to invalidate the cached offset widths if the geometry changes
this.lastUnscaledWidth = unscaledWidth;
};
/**
* Function: redraw
*
* Renders the text using the given DOM nodes.
*/
mxText.prototype.redraw = function()
{
if (this.visible && this.checkBounds() && this.cacheEnabled && this.lastValue == this.value &&
(mxUtils.isNode(this.value) || this.dialect == mxConstants.DIALECT_STRICTHTML))
{
if (this.node.nodeName == 'DIV' && (this.isHtmlAllowed() || !mxClient.IS_VML))
{
this.updateSize(this.node, (this.state == null || this.state.view.textDiv == null));
if (mxClient.IS_IE && (document.documentMode == null || document.documentMode <= 8))
{
this.updateHtmlFilter();
}
else
{
this.updateHtmlTransform();
}
this.updateBoundingBox();
}
else
{
var canvas = this.createCanvas();
if (canvas != null && canvas.updateText != null &&
canvas.invalidateCachedOffsetSize != null)
{
this.paint(canvas, true);
this.destroyCanvas(canvas);
this.updateBoundingBox();
}
else
{
// Fallback if canvas does not support updateText (VML)
mxShape.prototype.redraw.apply(this, arguments);
}
}
}
else
{
mxShape.prototype.redraw.apply(this, arguments);
if (mxUtils.isNode(this.value) || this.dialect == mxConstants.DIALECT_STRICTHTML)
{
this.lastValue = this.value;
}
else
{
this.lastValue = null;
}
}
};
/**
* Function: resetStyles
*
* Resets all styles.
*/
mxText.prototype.resetStyles = function()
{
mxShape.prototype.resetStyles.apply(this, arguments);
this.color = 'black';
this.align = mxConstants.ALIGN_CENTER;
this.valign = mxConstants.ALIGN_MIDDLE;
this.family = mxConstants.DEFAULT_FONTFAMILY;
this.size = mxConstants.DEFAULT_FONTSIZE;
this.fontStyle = mxConstants.DEFAULT_FONTSTYLE;
this.spacing = 2;
this.spacingTop = 2;
this.spacingRight = 2;
this.spacingBottom = 2;
this.spacingLeft = 2;
this.horizontal = true;
delete this.background;
delete this.border;
this.textDirection = mxConstants.DEFAULT_TEXT_DIRECTION;
delete this.margin;
};
/**
* Function: apply
*
* Extends mxShape to update the text styles.
*
* Parameters:
*
* state - <mxCellState> of the corresponding cell.
*/
mxText.prototype.apply = function(state)
{
var old = this.spacing;
mxShape.prototype.apply.apply(this, arguments);
if (this.style != null)
{
this.fontStyle = mxUtils.getValue(this.style, mxConstants.STYLE_FONTSTYLE, this.fontStyle);
this.family = mxUtils.getValue(this.style, mxConstants.STYLE_FONTFAMILY, this.family);
this.size = mxUtils.getValue(this.style, mxConstants.STYLE_FONTSIZE, this.size);
this.color = mxUtils.getValue(this.style, mxConstants.STYLE_FONTCOLOR, this.color);
this.align = mxUtils.getValue(this.style, mxConstants.STYLE_ALIGN, this.align);
this.valign = mxUtils.getValue(this.style, mxConstants.STYLE_VERTICAL_ALIGN, this.valign);
this.spacing = parseInt(mxUtils.getValue(this.style, mxConstants.STYLE_SPACING, this.spacing));
this.spacingTop = parseInt(mxUtils.getValue(this.style, mxConstants.STYLE_SPACING_TOP, this.spacingTop - old)) + this.spacing;
this.spacingRight = parseInt(mxUtils.getValue(this.style, mxConstants.STYLE_SPACING_RIGHT, this.spacingRight - old)) + this.spacing;
this.spacingBottom = parseInt(mxUtils.getValue(this.style, mxConstants.STYLE_SPACING_BOTTOM, this.spacingBottom - old)) + this.spacing;
this.spacingLeft = parseInt(mxUtils.getValue(this.style, mxConstants.STYLE_SPACING_LEFT, this.spacingLeft - old)) + this.spacing;
this.horizontal = mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, this.horizontal);
this.background = mxUtils.getValue(this.style, mxConstants.STYLE_LABEL_BACKGROUNDCOLOR, this.background);
this.border = mxUtils.getValue(this.style, mxConstants.STYLE_LABEL_BORDERCOLOR, this.border);
this.textDirection = mxUtils.getValue(this.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
this.opacity = mxUtils.getValue(this.style, mxConstants.STYLE_TEXT_OPACITY, 100);
this.updateMargin();
}
this.flipV = null;
this.flipH = null;
};
/**
* Function: getAutoDirection
*
* Used to determine the automatic text direction. Returns
* <mxConstants.TEXT_DIRECTION_LTR> or <mxConstants.TEXT_DIRECTION_RTL>
* depending on the contents of <value>. This is not invoked for HTML, wrapped
* content or if <value> is a DOM node.
*/
mxText.prototype.getAutoDirection = function()
{
// Looks for strong (directional) characters
var tmp = /[A-Za-z\u05d0-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(this.value);
// Returns the direction defined by the character
return (tmp != null && tmp.length > 0 && tmp[0] > 'z') ?
mxConstants.TEXT_DIRECTION_RTL : mxConstants.TEXT_DIRECTION_LTR;
};
/**
* Function: updateBoundingBox
*
* Updates the <boundingBox> for this shape using the given node and position.
*/
mxText.prototype.updateBoundingBox = function()
{
var node = this.node;
this.boundingBox = this.bounds.clone();
var rot = this.getTextRotation();
var h = (this.style != null) ? mxUtils.getValue(this.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER) : null;
var v = (this.style != null) ? mxUtils.getValue(this.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE) : null;
if (!this.ignoreStringSize && node != null && this.overflow != 'fill' && (!this.clipped ||
!this.ignoreClippedStringSize || h != mxConstants.ALIGN_CENTER || v != mxConstants.ALIGN_MIDDLE))
{
var ow = null;
var oh = null;
if (node.ownerSVGElement != null)
{
if (node.firstChild != null && node.firstChild.firstChild != null &&
node.firstChild.firstChild.nodeName == 'foreignObject')
{
node = node.firstChild.firstChild;
ow = parseInt(node.getAttribute('width')) * this.scale;
oh = parseInt(node.getAttribute('height')) * this.scale;
}
else
{
try
{
var b = node.getBBox();
// Workaround for bounding box of empty string
if (typeof(this.value) == 'string' && mxUtils.trim(this.value) == 0)
{
this.boundingBox = null;
}
else if (b.width == 0 && b.height == 0)
{
this.boundingBox = null;
}
else
{
this.boundingBox = new mxRectangle(b.x, b.y, b.width, b.height);
}
return;
}
catch (e)
{
// Ignores NS_ERROR_FAILURE in FF if container display is none.
}
}
}
else
{
var td = (this.state != null) ? this.state.view.textDiv : null;
// Use cached offset size
if (this.offsetWidth != null && this.offsetHeight != null)
{
ow = this.offsetWidth * this.scale;
oh = this.offsetHeight * this.scale;
}
else
{
// Cannot get node size while container hidden so a
// shared temporary DIV is used for text measuring
if (td != null)
{
this.updateFont(td);
this.updateSize(td, false);
this.updateInnerHtml(td);
node = td;
}
var sizeDiv = node;
if (document.documentMode == 8 && !mxClient.IS_EM)
{
var w = Math.round(this.bounds.width / this.scale);
if (this.wrap && w > 0)
{
node.style.wordWrap = mxConstants.WORD_WRAP;
node.style.whiteSpace = 'normal';
if (node.style.wordWrap != 'break-word')
{
// Innermost DIV is used for measuring text
var divs = sizeDiv.getElementsByTagName('div');
if (divs.length > 0)
{
sizeDiv = divs[divs.length - 1];
}
ow = sizeDiv.offsetWidth + 2;
divs = this.node.getElementsByTagName('div');
if (this.clipped)
{
ow = Math.min(w, ow);
}
// Second last DIV width must be updated in DOM tree
if (divs.length > 1)
{
divs[divs.length - 2].style.width = ow + 'px';
}
}
}
else
{
node.style.whiteSpace = 'nowrap';
}
}
else if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
}
this.offsetWidth = sizeDiv.offsetWidth + this.textWidthPadding;
this.offsetHeight = sizeDiv.offsetHeight;
ow = this.offsetWidth * this.scale;
oh = this.offsetHeight * this.scale;
}
}
if (ow != null && oh != null)
{
this.boundingBox = new mxRectangle(this.bounds.x,
this.bounds.y, ow, oh);
}
}
if (this.boundingBox != null)
{
if (rot != 0)
{
// Accounts for pre-rotated x and y
var bbox = mxUtils.getBoundingBox(new mxRectangle(
this.margin.x * this.boundingBox.width,
this.margin.y * this.boundingBox.height,
this.boundingBox.width, this.boundingBox.height),
rot, new mxPoint(0, 0));
this.unrotatedBoundingBox = mxRectangle.fromRectangle(this.boundingBox);
this.unrotatedBoundingBox.x += this.margin.x * this.unrotatedBoundingBox.width;
this.unrotatedBoundingBox.y += this.margin.y * this.unrotatedBoundingBox.height;
this.boundingBox.x += bbox.x;
this.boundingBox.y += bbox.y;
this.boundingBox.width = bbox.width;
this.boundingBox.height = bbox.height;
}
else
{
this.boundingBox.x += this.margin.x * this.boundingBox.width;
this.boundingBox.y += this.margin.y * this.boundingBox.height;
this.unrotatedBoundingBox = null;
}
}
};
/**
* Function: getShapeRotation
*
* Returns 0 to avoid using rotation in the canvas via updateTransform.
*/
mxText.prototype.getShapeRotation = function()
{
return 0;
};
/**
* Function: getTextRotation
*
* Returns the rotation for the text label of the corresponding shape.
*/
mxText.prototype.getTextRotation = function()
{
return (this.state != null && this.state.shape != null) ? this.state.shape.getTextRotation() : 0;
};
/**
* Function: isPaintBoundsInverted
*
* Inverts the bounds if <mxShape.isBoundsInverted> returns true or if the
* horizontal style is false.
*/
mxText.prototype.isPaintBoundsInverted = function()
{
return !this.horizontal && this.state != null && this.state.view.graph.model.isVertex(this.state.cell);
};
/**
* Function: configureCanvas
*
* Sets the state of the canvas for drawing the shape.
*/
mxText.prototype.configureCanvas = function(c, x, y, w, h)
{
mxShape.prototype.configureCanvas.apply(this, arguments);
c.setFontColor(this.color);
c.setFontBackgroundColor(this.background);
c.setFontBorderColor(this.border);
c.setFontFamily(this.family);
c.setFontSize(this.size);
c.setFontStyle(this.fontStyle);
};
/**
* Function: updateVmlContainer
*
* Sets the width and height of the container to 1px.
*/
mxText.prototype.updateVmlContainer = function()
{
this.node.style.left = Math.round(this.bounds.x) + 'px';
this.node.style.top = Math.round(this.bounds.y) + 'px';
this.node.style.width = '1px';
this.node.style.height = '1px';
this.node.style.overflow = 'visible';
};
/**
* Function: redrawHtmlShape
*
* Updates the HTML node(s) to reflect the latest bounds and scale.
*/
mxText.prototype.redrawHtmlShape = function()
{
var style = this.node.style;
// Resets CSS styles
style.whiteSpace = 'normal';
style.overflow = '';
style.width = '';
style.height = '';
this.updateValue();
this.updateFont(this.node);
this.updateSize(this.node, (this.state == null || this.state.view.textDiv == null));
this.offsetWidth = null;
this.offsetHeight = null;
if (mxClient.IS_IE && (document.documentMode == null || document.documentMode <= 8))
{
this.updateHtmlFilter();
}
else
{
this.updateHtmlTransform();
}
};
/**
* Function: updateHtmlTransform
*
* Returns the spacing as an <mxPoint>.
*/
mxText.prototype.updateHtmlTransform = function()
{
var theta = this.getTextRotation();
var style = this.node.style;
var dx = this.margin.x;
var dy = this.margin.y;
if (theta != 0)
{
mxUtils.setPrefixedStyle(style, 'transformOrigin', (-dx * 100) + '%' + ' ' + (-dy * 100) + '%');
mxUtils.setPrefixedStyle(style, 'transform', 'translate(' + (dx * 100) + '%' + ',' + (dy * 100) + '%)' +
'scale(' + this.scale + ') rotate(' + theta + 'deg)');
}
else
{
mxUtils.setPrefixedStyle(style, 'transformOrigin', '0% 0%');
mxUtils.setPrefixedStyle(style, 'transform', 'scale(' + this.scale + ')' +
'translate(' + (dx * 100) + '%' + ',' + (dy * 100) + '%)');
}
style.left = Math.round(this.bounds.x - Math.ceil(dx * ((this.overflow != 'fill' &&
this.overflow != 'width') ? 3 : 1))) + 'px';
style.top = Math.round(this.bounds.y - dy * ((this.overflow != 'fill') ? 3 : 1)) + 'px';
if (this.opacity < 100)
{
style.opacity = this.opacity / 100;
}
else
{
style.opacity = '';
}
};
/**
* Function: setInnerHtml
*
* Sets the inner HTML of the given element to the <value>.
*/
mxText.prototype.updateInnerHtml = function(elt)
{
if (mxUtils.isNode(this.value))
{
elt.innerHTML = this.value.outerHTML;
}
else
{
var val = this.value;
if (this.dialect != mxConstants.DIALECT_STRICTHTML)
{
// LATER: Can be cached in updateValue
val = mxUtils.htmlEntities(val, false);
}
// Handles trailing newlines to make sure they are visible in rendering output
val = mxUtils.replaceTrailingNewlines(val, '<div>&nbsp;</div>');
val = (this.replaceLinefeeds) ? val.replace(/\n/g, '<br/>') : val;
val = '<div style="display:inline-block;_display:inline;">' + val + '</div>';
elt.innerHTML = val;
}
};
/**
* Function: updateHtmlFilter
*
* Rotated text rendering quality is bad for IE9 quirks/IE8 standards
*/
mxText.prototype.updateHtmlFilter = function()
{
var style = this.node.style;
var dx = this.margin.x;
var dy = this.margin.y;
var s = this.scale;
// Resets filter before getting offsetWidth
mxUtils.setOpacity(this.node, this.opacity);
// Adds 1 to match table height in 1.x
var ow = 0;
var oh = 0;
var td = (this.state != null) ? this.state.view.textDiv : null;
var sizeDiv = this.node;
// Fallback for hidden text rendering in IE quirks mode
if (td != null)
{
td.style.overflow = '';
td.style.height = '';
td.style.width = '';
this.updateFont(td);
this.updateSize(td, false);
this.updateInnerHtml(td);
var w = Math.round(this.bounds.width / this.scale);
if (this.wrap && w > 0)
{
td.style.whiteSpace = 'normal';
td.style.wordWrap = mxConstants.WORD_WRAP;
ow = w;
if (this.clipped)
{
ow = Math.min(ow, this.bounds.width);
}
td.style.width = ow + 'px';
}
else
{
td.style.whiteSpace = 'nowrap';
}
sizeDiv = td;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
if (this.wrap && td.style.wordWrap == 'break-word')
{
sizeDiv.style.width = '100%';
}
}
// Required to update the height of the text box after wrapping width is known
if (!this.clipped && this.wrap && w > 0)
{
ow = sizeDiv.offsetWidth + this.textWidthPadding;
td.style.width = ow + 'px';
}
oh = sizeDiv.offsetHeight + 2;
if (mxClient.IS_QUIRKS && this.border != null && this.border != mxConstants.NONE)
{
oh += 3;
}
}
else if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
oh = sizeDiv.offsetHeight;
}
ow = sizeDiv.offsetWidth + this.textWidthPadding;
if (this.clipped)
{
oh = Math.min(oh, this.bounds.height);
}
var w = this.bounds.width / s;
var h = this.bounds.height / s;
// Handles special case for live preview with no wrapper DIV and no textDiv
if (this.overflow == 'fill')
{
oh = h;
ow = w;
}
else if (this.overflow == 'width')
{
oh = sizeDiv.scrollHeight;
ow = w;
}
// Stores for later use
this.offsetWidth = ow;
this.offsetHeight = oh;
// Simulates max-height CSS in quirks mode
if (mxClient.IS_QUIRKS && (this.clipped || (this.overflow == 'width' && h > 0)))
{
h = Math.min(h, oh);
style.height = Math.round(h) + 'px';
}
else
{
h = oh;
}
if (this.overflow != 'fill' && this.overflow != 'width')
{
if (this.clipped)
{
ow = Math.min(w, ow);
}
w = ow;
// Simulates max-width CSS in quirks mode
if ((mxClient.IS_QUIRKS && this.clipped) || this.wrap)
{
style.width = Math.round(w) + 'px';
}
}
h *= s;
w *= s;
// Rotation case is handled via VML canvas
var rad = this.getTextRotation() * (Math.PI / 180);
// Precalculate cos and sin for the rotation
var real_cos = parseFloat(parseFloat(Math.cos(rad)).toFixed(8));
var real_sin = parseFloat(parseFloat(Math.sin(-rad)).toFixed(8));
rad %= 2 * Math.PI;
if (rad < 0)
{
rad += 2 * Math.PI;
}
rad %= Math.PI;
if (rad > Math.PI / 2)
{
rad = Math.PI - rad;
}
var cos = Math.cos(rad);
var sin = Math.sin(-rad);
var tx = w * -(dx + 0.5);
var ty = h * -(dy + 0.5);
var top_fix = (h - h * cos + w * sin) / 2 + real_sin * tx - real_cos * ty;
var left_fix = (w - w * cos + h * sin) / 2 - real_cos * tx - real_sin * ty;
if (rad != 0)
{
var f = 'progid:DXImageTransform.Microsoft.Matrix(M11=' + real_cos + ', M12='+
real_sin + ', M21=' + (-real_sin) + ', M22=' + real_cos + ', sizingMethod=\'auto expand\')';
if (style.filter != null && style.filter.length > 0)
{
style.filter += ' ' + f;
}
else
{
style.filter = f;
}
}
// Workaround for rendering offsets
var dy = 0;
if (this.overflow != 'fill' && mxClient.IS_QUIRKS)
{
if (this.valign == mxConstants.ALIGN_TOP)
{
dy -= 1;
}
else if (this.valign == mxConstants.ALIGN_BOTTOM)
{
dy += 2;
}
else
{
dy += 1;
}
}
style.zoom = s;
style.left = Math.round(this.bounds.x + left_fix - w / 2) + 'px';
style.top = Math.round(this.bounds.y + top_fix - h / 2 + dy) + 'px';
};
/**
* Function: updateValue
*
* Updates the HTML node(s) to reflect the latest bounds and scale.
*/
mxText.prototype.updateValue = function()
{
if (mxUtils.isNode(this.value))
{
this.node.innerHTML = '';
this.node.appendChild(this.value);
}
else
{
var val = this.value;
if (this.dialect != mxConstants.DIALECT_STRICTHTML)
{
val = mxUtils.htmlEntities(val, false);
}
// Handles trailing newlines to make sure they are visible in rendering output
val = mxUtils.replaceTrailingNewlines(val, '<div><br></div>');
val = (this.replaceLinefeeds) ? val.replace(/\n/g, '<br/>') : val;
var bg = (this.background != null && this.background != mxConstants.NONE) ? this.background : null;
var bd = (this.border != null && this.border != mxConstants.NONE) ? this.border : null;
if (this.overflow == 'fill' || this.overflow == 'width')
{
if (bg != null)
{
this.node.style.backgroundColor = bg;
}
if (bd != null)
{
this.node.style.border = '1px solid ' + bd;
}
}
else
{
var css = '';
if (bg != null)
{
css += 'background-color:' + mxUtils.htmlEntities(bg) + ';';
}
if (bd != null)
{
css += 'border:1px solid ' + mxUtils.htmlEntities(bd) + ';';
}
// Wrapper DIV for background, zoom needed for inline in quirks
// and to measure wrapped font sizes in all browsers
// FIXME: Background size in quirks mode for wrapped text
var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (this.size * mxConstants.LINE_HEIGHT) + 'px' :
mxConstants.LINE_HEIGHT;
val = '<div style="zoom:1;' + css + 'display:inline-block;_display:inline;text-decoration:inherit;' +
'padding-bottom:1px;padding-right:1px;line-height:' + lh + '">' + val + '</div>';
}
this.node.innerHTML = val;
// Sets text direction
var divs = this.node.getElementsByTagName('div');
if (divs.length > 0)
{
var dir = this.textDirection;
if (dir == mxConstants.TEXT_DIRECTION_AUTO && this.dialect != mxConstants.DIALECT_STRICTHTML)
{
dir = this.getAutoDirection();
}
if (dir == mxConstants.TEXT_DIRECTION_LTR || dir == mxConstants.TEXT_DIRECTION_RTL)
{
divs[divs.length - 1].setAttribute('dir', dir);
}
else
{
divs[divs.length - 1].removeAttribute('dir');
}
}
}
};
/**
* Function: updateFont
*
* Updates the HTML node(s) to reflect the latest bounds and scale.
*/
mxText.prototype.updateFont = function(node)
{
var style = node.style;
style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (this.size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT;
style.fontSize = this.size + 'px';
style.fontFamily = this.family;
style.verticalAlign = 'top';
style.color = this.color;
if ((this.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
{
style.fontWeight = 'bold';
}
else
{
style.fontWeight = '';
}
if ((this.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
{
style.fontStyle = 'italic';
}
else
{
style.fontStyle = '';
}
if ((this.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
{
style.textDecoration = 'underline';
}
else
{
style.textDecoration = '';
}
if (this.align == mxConstants.ALIGN_CENTER)
{
style.textAlign = 'center';
}
else if (this.align == mxConstants.ALIGN_RIGHT)
{
style.textAlign = 'right';
}
else
{
style.textAlign = 'left';
}
};
/**
* Function: updateSize
*
* Updates the HTML node(s) to reflect the latest bounds and scale.
*/
mxText.prototype.updateSize = function(node, enableWrap)
{
var w = Math.max(0, Math.round(this.bounds.width / this.scale));
var h = Math.max(0, Math.round(this.bounds.height / this.scale));
var style = node.style;
// NOTE: Do not use maxWidth here because wrapping will
// go wrong if the cell is outside of the viewable area
if (this.clipped)
{
style.overflow = 'hidden';
if (!mxClient.IS_QUIRKS)
{
style.maxHeight = h + 'px';
style.maxWidth = w + 'px';
}
else
{
style.width = w + 'px';
}
}
else if (this.overflow == 'fill')
{
style.width = (w + 1) + 'px';
style.height = (h + 1) + 'px';
style.overflow = 'hidden';
}
else if (this.overflow == 'width')
{
style.width = (w + 1) + 'px';
style.maxHeight = (h + 1) + 'px';
style.overflow = 'hidden';
}
if (this.wrap && w > 0)
{
style.wordWrap = mxConstants.WORD_WRAP;
style.whiteSpace = 'normal';
style.width = w + 'px';
if (enableWrap && this.overflow != 'fill' && this.overflow != 'width')
{
var sizeDiv = node;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV')
{
sizeDiv = sizeDiv.firstChild;
if (node.style.wordWrap == 'break-word')
{
sizeDiv.style.width = '100%';
}
}
var tmp = sizeDiv.offsetWidth;
// Workaround for text measuring in hidden containers
if (tmp == 0)
{
var prev = node.parentNode;
node.style.visibility = 'hidden';
document.body.appendChild(node);
tmp = sizeDiv.offsetWidth;
node.style.visibility = '';
prev.appendChild(node);
}
tmp += 3;
if (this.clipped)
{
tmp = Math.min(tmp, w);
}
style.width = tmp + 'px';
}
}
else
{
style.whiteSpace = 'nowrap';
}
};
/**
* Function: getMargin
*
* Returns the spacing as an <mxPoint>.
*/
mxText.prototype.updateMargin = function()
{
this.margin = mxUtils.getAlignmentAsPoint(this.align, this.valign);
};
/**
* Function: getSpacing
*
* Returns the spacing as an <mxPoint>.
*/
mxText.prototype.getSpacing = function()
{
var dx = 0;
var dy = 0;
if (this.align == mxConstants.ALIGN_CENTER)
{
dx = (this.spacingLeft - this.spacingRight) / 2;
}
else if (this.align == mxConstants.ALIGN_RIGHT)
{
dx = -this.spacingRight - this.baseSpacingRight;
}
else
{
dx = this.spacingLeft + this.baseSpacingLeft;
}
if (this.valign == mxConstants.ALIGN_MIDDLE)
{
dy = (this.spacingTop - this.spacingBottom) / 2;
}
else if (this.valign == mxConstants.ALIGN_BOTTOM)
{
dy = -this.spacingBottom - this.baseSpacingBottom;;
}
else
{
dy = this.spacingTop + this.baseSpacingTop;
}
return new mxPoint(dx, dy);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxTriangle
*
* Implementation of the triangle shape.
*
* Constructor: mxTriangle
*
* Constructs a new triangle shape.
*/
function mxTriangle()
{
mxActor.call(this);
};
/**
* Extends mxActor.
*/
mxUtils.extend(mxTriangle, mxActor);
/**
* Function: isRoundable
*
* Adds roundable support.
*/
mxTriangle.prototype.isRoundable = function()
{
return true;
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxTriangle.prototype.redrawPath = function(c, x, y, w, h)
{
var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2;
this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w, 0.5 * h), new mxPoint(0, h)], this.isRounded, arcSize, true);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxHexagon
*
* Implementation of the hexagon shape.
*
* Constructor: mxHexagon
*
* Constructs a new hexagon shape.
*/
function mxHexagon()
{
mxActor.call(this);
};
/**
* Extends mxActor.
*/
mxUtils.extend(mxHexagon, mxActor);
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxHexagon.prototype.redrawPath = function(c, x, y, w, h)
{
var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2;
this.addPoints(c, [new mxPoint(0.25 * w, 0), new mxPoint(0.75 * w, 0), new mxPoint(w, 0.5 * h), new mxPoint(0.75 * w, h),
new mxPoint(0.25 * w, h), new mxPoint(0, 0.5 * h)], this.isRounded, arcSize, true);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxLine
*
* Extends <mxShape> to implement a horizontal line shape.
* This shape is registered under <mxConstants.SHAPE_LINE> in
* <mxCellRenderer>.
*
* Constructor: mxLine
*
* Constructs a new line shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* stroke - String that defines the stroke color. Default is 'black'. This is
* stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxLine(bounds, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxLine, mxShape);
/**
* Function: paintVertexShape
*
* Redirects to redrawPath for subclasses to work.
*/
mxLine.prototype.paintVertexShape = function(c, x, y, w, h)
{
var mid = y + h / 2;
c.begin();
c.moveTo(x, mid);
c.lineTo(x + w, mid);
c.stroke();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxImageShape
*
* Extends <mxShape> to implement an image shape. This shape is registered
* under <mxConstants.SHAPE_IMAGE> in <mxCellRenderer>.
*
* Constructor: mxImageShape
*
* Constructs a new image shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* image - String that specifies the URL of the image. This is stored in
* <image>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 0. This is stored in <strokewidth>.
*/
function mxImageShape(bounds, image, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.image = image;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
this.shadow = false;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxImageShape, mxRectangleShape);
/**
* Variable: preserveImageAspect
*
* Switch to preserve image aspect. Default is true.
*/
mxImageShape.prototype.preserveImageAspect = true;
/**
* Function: getSvgScreenOffset
*
* Disables offset in IE9 for crisper image output.
*/
mxImageShape.prototype.getSvgScreenOffset = function()
{
return 0;
};
/**
* Function: apply
*
* Overrides <mxShape.apply> to replace the fill and stroke colors with the
* respective values from <mxConstants.STYLE_IMAGE_BACKGROUND> and
* <mxConstants.STYLE_IMAGE_BORDER>.
*
* Applies the style of the given <mxCellState> to the shape. This
* implementation assigns the following styles to local fields:
*
* - <mxConstants.STYLE_IMAGE_BACKGROUND> => fill
* - <mxConstants.STYLE_IMAGE_BORDER> => stroke
*
* Parameters:
*
* state - <mxCellState> of the corresponding cell.
*/
mxImageShape.prototype.apply = function(state)
{
mxShape.prototype.apply.apply(this, arguments);
this.fill = null;
this.stroke = null;
this.gradient = null;
if (this.style != null)
{
this.preserveImageAspect = mxUtils.getNumber(this.style, mxConstants.STYLE_IMAGE_ASPECT, 1) == 1;
// Legacy support for imageFlipH/V
this.flipH = this.flipH || mxUtils.getValue(this.style, 'imageFlipH', 0) == 1;
this.flipV = this.flipV || mxUtils.getValue(this.style, 'imageFlipV', 0) == 1;
}
};
/**
* Function: isHtmlAllowed
*
* Returns true if HTML is allowed for this shape. This implementation always
* returns false.
*/
mxImageShape.prototype.isHtmlAllowed = function()
{
return !this.preserveImageAspect;
};
/**
* Function: createHtml
*
* Creates and returns the HTML DOM node(s) to represent
* this shape. This implementation falls back to <createVml>
* so that the HTML creation is optional.
*/
mxImageShape.prototype.createHtml = function()
{
var node = document.createElement('div');
node.style.position = 'absolute';
return node;
};
/**
* Function: isRoundable
*
* Disables inherited roundable support.
*/
mxImageShape.prototype.isRoundable = function(c, x, y, w, h)
{
return false;
};
/**
* Function: paintVertexShape
*
* Generic background painting implementation.
*/
mxImageShape.prototype.paintVertexShape = function(c, x, y, w, h)
{
if (this.image != null)
{
var fill = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BACKGROUND, null);
var stroke = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BORDER, null);
if (fill != null)
{
// Stroke rendering required for shadow
c.setFillColor(fill);
c.setStrokeColor(stroke);
c.rect(x, y, w, h);
c.fillAndStroke();
}
// FlipH/V are implicit via mxShape.updateTransform
c.image(x, y, w, h, this.image, this.preserveImageAspect, false, false);
var stroke = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BORDER, null);
if (stroke != null)
{
c.setShadow(false);
c.setStrokeColor(stroke);
c.rect(x, y, w, h);
c.stroke();
}
}
else
{
mxRectangleShape.prototype.paintBackground.apply(this, arguments);
}
};
/**
* Function: redraw
*
* Overrides <mxShape.redraw> to preserve the aspect ratio of images.
*/
mxImageShape.prototype.redrawHtmlShape = function()
{
this.node.style.left = Math.round(this.bounds.x) + 'px';
this.node.style.top = Math.round(this.bounds.y) + 'px';
this.node.style.width = Math.max(0, Math.round(this.bounds.width)) + 'px';
this.node.style.height = Math.max(0, Math.round(this.bounds.height)) + 'px';
this.node.innerHTML = '';
if (this.image != null)
{
var fill = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BACKGROUND, '');
var stroke = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_BORDER, '');
this.node.style.backgroundColor = fill;
this.node.style.borderColor = stroke;
// VML image supports PNG in IE6
var useVml = mxClient.IS_IE6 || ((document.documentMode == null || document.documentMode <= 8) && this.rotation != 0);
var img = document.createElement((useVml) ? mxClient.VML_PREFIX + ':image' : 'img');
img.setAttribute('border', '0');
img.style.position = 'absolute';
img.src = this.image;
var filter = (this.opacity < 100) ? 'alpha(opacity=' + this.opacity + ')' : '';
this.node.style.filter = filter;
if (this.flipH && this.flipV)
{
filter += 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';
}
else if (this.flipH)
{
filter += 'progid:DXImageTransform.Microsoft.BasicImage(mirror=1)';
}
else if (this.flipV)
{
filter += 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)';
}
if (img.style.filter != filter)
{
img.style.filter = filter;
}
if (img.nodeName == 'image')
{
img.style.rotation = this.rotation;
}
else if (this.rotation != 0)
{
// LATER: Add flipV/H support
mxUtils.setPrefixedStyle(img.style, 'transform', 'rotate(' + this.rotation + 'deg)');
}
else
{
mxUtils.setPrefixedStyle(img.style, 'transform', '');
}
// Known problem: IE clips top line of image for certain angles
img.style.width = this.node.style.width;
img.style.height = this.node.style.height;
this.node.style.backgroundImage = '';
this.node.appendChild(img);
}
else
{
this.setTransparentBackgroundImage(this.node);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxLabel
*
* Extends <mxShape> to implement an image shape with a label.
* This shape is registered under <mxConstants.SHAPE_LABEL> in
* <mxCellRenderer>.
*
* Constructor: mxLabel
*
* Constructs a new label shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxLabel(bounds, fill, stroke, strokewidth)
{
mxRectangleShape.call(this, bounds, fill, stroke, strokewidth);
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxLabel, mxRectangleShape);
/**
* Variable: imageSize
*
* Default width and height for the image. Default is
* <mxConstants.DEFAULT_IMAGESIZE>.
*/
mxLabel.prototype.imageSize = mxConstants.DEFAULT_IMAGESIZE;
/**
* Variable: spacing
*
* Default value for image spacing. Default is 2.
*/
mxLabel.prototype.spacing = 2;
/**
* Variable: indicatorSize
*
* Default width and height for the indicicator. Default is 10.
*/
mxLabel.prototype.indicatorSize = 10;
/**
* Variable: indicatorSpacing
*
* Default spacing between image and indicator. Default is 2.
*/
mxLabel.prototype.indicatorSpacing = 2;
/**
* Function: init
*
* Initializes the shape and the <indicator>.
*/
mxLabel.prototype.init = function(container)
{
mxShape.prototype.init.apply(this, arguments);
if (this.indicatorShape != null)
{
this.indicator = new this.indicatorShape();
this.indicator.dialect = this.dialect;
this.indicator.init(this.node);
}
};
/**
* Function: redraw
*
* Reconfigures this shape. This will update the colors of the indicator
* and reconfigure it if required.
*/
mxLabel.prototype.redraw = function()
{
if (this.indicator != null)
{
this.indicator.fill = this.indicatorColor;
this.indicator.stroke = this.indicatorStrokeColor;
this.indicator.gradient = this.indicatorGradientColor;
this.indicator.direction = this.indicatorDirection;
}
mxShape.prototype.redraw.apply(this, arguments);
};
/**
* Function: isHtmlAllowed
*
* Returns true for non-rounded, non-rotated shapes with no glass gradient and
* no indicator shape.
*/
mxLabel.prototype.isHtmlAllowed = function()
{
return mxRectangleShape.prototype.isHtmlAllowed.apply(this, arguments) &&
this.indicatorColor == null && this.indicatorShape == null;
};
/**
* Function: paintForeground
*
* Generic background painting implementation.
*/
mxLabel.prototype.paintForeground = function(c, x, y, w, h)
{
this.paintImage(c, x, y, w, h);
this.paintIndicator(c, x, y, w, h);
mxRectangleShape.prototype.paintForeground.apply(this, arguments);
};
/**
* Function: paintImage
*
* Generic background painting implementation.
*/
mxLabel.prototype.paintImage = function(c, x, y, w, h)
{
if (this.image != null)
{
var bounds = this.getImageBounds(x, y, w, h);
c.image(bounds.x, bounds.y, bounds.width, bounds.height, this.image, false, false, false);
}
};
/**
* Function: getImageBounds
*
* Generic background painting implementation.
*/
mxLabel.prototype.getImageBounds = function(x, y, w, h)
{
var align = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT);
var valign = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE);
var width = mxUtils.getNumber(this.style, mxConstants.STYLE_IMAGE_WIDTH, mxConstants.DEFAULT_IMAGESIZE);
var height = mxUtils.getNumber(this.style, mxConstants.STYLE_IMAGE_HEIGHT, mxConstants.DEFAULT_IMAGESIZE);
var spacing = mxUtils.getNumber(this.style, mxConstants.STYLE_SPACING, this.spacing) + 5;
if (align == mxConstants.ALIGN_CENTER)
{
x += (w - width) / 2;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
x += w - width - spacing;
}
else // default is left
{
x += spacing;
}
if (valign == mxConstants.ALIGN_TOP)
{
y += spacing;
}
else if (valign == mxConstants.ALIGN_BOTTOM)
{
y += h - height - spacing;
}
else // default is middle
{
y += (h - height) / 2;
}
return new mxRectangle(x, y, width, height);
};
/**
* Function: paintIndicator
*
* Generic background painting implementation.
*/
mxLabel.prototype.paintIndicator = function(c, x, y, w, h)
{
if (this.indicator != null)
{
this.indicator.bounds = this.getIndicatorBounds(x, y, w, h);
this.indicator.paint(c);
}
else if (this.indicatorImage != null)
{
var bounds = this.getIndicatorBounds(x, y, w, h);
c.image(bounds.x, bounds.y, bounds.width, bounds.height, this.indicatorImage, false, false, false);
}
};
/**
* Function: getIndicatorBounds
*
* Generic background painting implementation.
*/
mxLabel.prototype.getIndicatorBounds = function(x, y, w, h)
{
var align = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT);
var valign = mxUtils.getValue(this.style, mxConstants.STYLE_IMAGE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE);
var width = mxUtils.getNumber(this.style, mxConstants.STYLE_INDICATOR_WIDTH, this.indicatorSize);
var height = mxUtils.getNumber(this.style, mxConstants.STYLE_INDICATOR_HEIGHT, this.indicatorSize);
var spacing = this.spacing + 5;
if (align == mxConstants.ALIGN_RIGHT)
{
x += w - width - spacing;
}
else if (align == mxConstants.ALIGN_CENTER)
{
x += (w - width) / 2;
}
else // default is left
{
x += spacing;
}
if (valign == mxConstants.ALIGN_BOTTOM)
{
y += h - height - spacing;
}
else if (valign == mxConstants.ALIGN_TOP)
{
y += spacing;
}
else // default is middle
{
y += (h - height) / 2;
}
return new mxRectangle(x, y, width, height);
};
/**
* Function: redrawHtmlShape
*
* Generic background painting implementation.
*/
mxLabel.prototype.redrawHtmlShape = function()
{
mxRectangleShape.prototype.redrawHtmlShape.apply(this, arguments);
// Removes all children
while(this.node.hasChildNodes())
{
this.node.removeChild(this.node.lastChild);
}
if (this.image != null)
{
var node = document.createElement('img');
node.style.position = 'relative';
node.setAttribute('border', '0');
var bounds = this.getImageBounds(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height);
bounds.x -= this.bounds.x;
bounds.y -= this.bounds.y;
node.style.left = Math.round(bounds.x) + 'px';
node.style.top = Math.round(bounds.y) + 'px';
node.style.width = Math.round(bounds.width) + 'px';
node.style.height = Math.round(bounds.height) + 'px';
node.src = this.image;
this.node.appendChild(node);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCylinder
*
* Extends <mxShape> to implement an cylinder shape. If a
* custom shape with one filled area and an overlay path is
* needed, then this shape's <redrawPath> should be overridden.
* This shape is registered under <mxConstants.SHAPE_CYLINDER>
* in <mxCellRenderer>.
*
* Constructor: mxCylinder
*
* Constructs a new cylinder shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxCylinder(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxCylinder, mxShape);
/**
* Variable: maxHeight
*
* Defines the maximum height of the top and bottom part
* of the cylinder shape.
*/
mxCylinder.prototype.maxHeight = 40;
/**
* Variable: svgStrokeTolerance
*
* Sets stroke tolerance to 0 for SVG.
*/
mxCylinder.prototype.svgStrokeTolerance = 0;
/**
* Function: paintVertexShape
*
* Redirects to redrawPath for subclasses to work.
*/
mxCylinder.prototype.paintVertexShape = function(c, x, y, w, h)
{
c.translate(x, y);
c.begin();
this.redrawPath(c, x, y, w, h, false);
c.fillAndStroke();
if (!this.outline || this.style == null || mxUtils.getValue(
this.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0)
{
c.setShadow(false);
c.begin();
this.redrawPath(c, x, y, w, h, true);
c.stroke();
}
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxCylinder.prototype.getCylinderSize = function(x, y, w, h)
{
return Math.min(this.maxHeight, Math.round(h / 5));
};
/**
* Function: redrawPath
*
* Draws the path for this shape.
*/
mxCylinder.prototype.redrawPath = function(c, x, y, w, h, isForeground)
{
var dy = this.getCylinderSize(x, y, w, h);
if ((isForeground && this.fill != null) || (!isForeground && this.fill == null))
{
c.moveTo(0, dy);
c.curveTo(0, 2 * dy, w, 2 * dy, w, dy);
// Needs separate shapes for correct hit-detection
if (!isForeground)
{
c.stroke();
c.begin();
}
}
if (!isForeground)
{
c.moveTo(0, dy);
c.curveTo(0, -dy / 3, w, -dy / 3, w, dy);
c.lineTo(w, h - dy);
c.curveTo(w, h + dy / 3, 0, h + dy / 3, 0, h - dy);
c.close();
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConnector
*
* Extends <mxShape> to implement a connector shape. The connector
* shape allows for arrow heads on either side.
*
* This shape is registered under <mxConstants.SHAPE_CONNECTOR> in
* <mxCellRenderer>.
*
* Constructor: mxConnector
*
* Constructs a new connector shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* Default is 'black'.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxConnector(points, stroke, strokewidth)
{
mxPolyline.call(this, points, stroke, strokewidth);
};
/**
* Extends mxPolyline.
*/
mxUtils.extend(mxConnector, mxPolyline);
/**
* Function: updateBoundingBox
*
* Updates the <boundingBox> for this shape using <createBoundingBox> and
* <augmentBoundingBox> and stores the result in <boundingBox>.
*/
mxConnector.prototype.updateBoundingBox = function()
{
this.useSvgBoundingBox = this.style != null && this.style[mxConstants.STYLE_CURVED] == 1;
mxShape.prototype.updateBoundingBox.apply(this, arguments);
};
/**
* Function: paintEdgeShape
*
* Paints the line shape.
*/
mxConnector.prototype.paintEdgeShape = function(c, pts)
{
// The indirection via functions for markers is needed in
// order to apply the offsets before painting the line and
// paint the markers after painting the line.
var sourceMarker = this.createMarker(c, pts, true);
var targetMarker = this.createMarker(c, pts, false);
mxPolyline.prototype.paintEdgeShape.apply(this, arguments);
// Disables shadows, dashed styles and fixes fill color for markers
c.setFillColor(this.stroke);
c.setShadow(false);
c.setDashed(false);
if (sourceMarker != null)
{
sourceMarker();
}
if (targetMarker != null)
{
targetMarker();
}
};
/**
* Function: createMarker
*
* Prepares the marker by adding offsets in pts and returning a function to
* paint the marker.
*/
mxConnector.prototype.createMarker = function(c, pts, source)
{
var result = null;
var n = pts.length;
var type = mxUtils.getValue(this.style, (source) ? mxConstants.STYLE_STARTARROW : mxConstants.STYLE_ENDARROW);
var p0 = (source) ? pts[1] : pts[n - 2];
var pe = (source) ? pts[0] : pts[n - 1];
if (type != null && p0 != null && pe != null)
{
var count = 1;
// Uses next non-overlapping point
while (count < n - 1 && Math.round(p0.x - pe.x) == 0 && Math.round(p0.y - pe.y) == 0)
{
p0 = (source) ? pts[1 + count] : pts[n - 2 - count];
count++;
}
// Computes the norm and the inverse norm
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
var unitX = dx / dist;
var unitY = dy / dist;
var size = mxUtils.getNumber(this.style, (source) ? mxConstants.STYLE_STARTSIZE : mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE);
// Allow for stroke width in the end point used and the
// orthogonal vectors describing the direction of the marker
var filled = this.style[(source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL] != 0;
result = mxMarker.createMarker(c, this, type, pe, unitX, unitY, size, source, this.strokewidth, filled);
}
return result;
};
/**
* Function: augmentBoundingBox
*
* Augments the bounding box with the strokewidth and shadow offsets.
*/
mxConnector.prototype.augmentBoundingBox = function(bbox)
{
mxShape.prototype.augmentBoundingBox.apply(this, arguments);
// Adds marker sizes
var size = 0;
if (mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE)
{
size = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE) + 1;
}
if (mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE)
{
size = Math.max(size, mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE)) + 1;
}
bbox.grow(size * this.scale);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlane
*
* Extends <mxShape> to implement a swimlane shape. This shape is registered
* under <mxConstants.SHAPE_SWIMLANE> in <mxCellRenderer>. Use the
* <mxConstants.STYLE_STYLE_STARTSIZE> to define the size of the title
* region, <mxConstants.STYLE_SWIMLANE_FILLCOLOR> for the content area fill,
* <mxConstants.STYLE_SEPARATORCOLOR> to draw an additional vertical separator
* and <mxConstants.STYLE_SWIMLANE_LINE> to hide the line between the title
* region and the content area. The <mxConstants.STYLE_HORIZONTAL> affects
* the orientation of this shape, not only its label.
*
* Constructor: mxSwimlane
*
* Constructs a new swimlane shape.
*
* Parameters:
*
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
*/
function mxSwimlane(bounds, fill, stroke, strokewidth)
{
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
};
/**
* Extends mxShape.
*/
mxUtils.extend(mxSwimlane, mxShape);
/**
* Variable: imageSize
*
* Default imagewidth and imageheight if an image but no imagewidth
* and imageheight are defined in the style. Value is 16.
*/
mxSwimlane.prototype.imageSize = 16;
/**
* Function: isRoundable
*
* Adds roundable support.
*/
mxSwimlane.prototype.isRoundable = function(c, x, y, w, h)
{
return true;
};
/**
* Function: getGradientBounds
*
* Returns the bounding box for the gradient box for this shape.
*/
mxSwimlane.prototype.getTitleSize = function()
{
return Math.max(0, mxUtils.getValue(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE));
};
/**
* Function: getGradientBounds
*
* Returns the bounding box for the gradient box for this shape.
*/
mxSwimlane.prototype.getLabelBounds = function(rect)
{
var start = this.getTitleSize();
var bounds = new mxRectangle(rect.x, rect.y, rect.width, rect.height);
var horizontal = this.isHorizontal();
var flipH = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPH, 0) == 1;
var flipV = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPV, 0) == 1;
// East is default
var shapeVertical = (this.direction == mxConstants.DIRECTION_NORTH ||
this.direction == mxConstants.DIRECTION_SOUTH);
var realHorizontal = horizontal == !shapeVertical;
var realFlipH = !realHorizontal && flipH != (this.direction == mxConstants.DIRECTION_SOUTH ||
this.direction == mxConstants.DIRECTION_WEST);
var realFlipV = realHorizontal && flipV != (this.direction == mxConstants.DIRECTION_SOUTH ||
this.direction == mxConstants.DIRECTION_WEST);
// Shape is horizontal
if (!shapeVertical)
{
var tmp = Math.min(bounds.height, start * this.scale);
if (realFlipH || realFlipV)
{
bounds.y += bounds.height - tmp;
}
bounds.height = tmp;
}
else
{
var tmp = Math.min(bounds.width, start * this.scale);
if (realFlipH || realFlipV)
{
bounds.x += bounds.width - tmp;
}
bounds.width = tmp;
}
return bounds;
};
/**
* Function: getGradientBounds
*
* Returns the bounding box for the gradient box for this shape.
*/
mxSwimlane.prototype.getGradientBounds = function(c, x, y, w, h)
{
var start = this.getTitleSize();
if (this.isHorizontal())
{
start = Math.min(start, h);
return new mxRectangle(x, y, w, start);
}
else
{
start = Math.min(start, w);
return new mxRectangle(x, y, start, h);
}
};
/**
* Function: getArcSize
*
* Returns the arcsize for the swimlane.
*/
mxSwimlane.prototype.getArcSize = function(w, h, start)
{
var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100;
return start * f * 3;
};
/**
* Function: paintVertexShape
*
* Paints the swimlane vertex shape.
*/
mxSwimlane.prototype.isHorizontal = function()
{
return mxUtils.getValue(this.style, mxConstants.STYLE_HORIZONTAL, 1) == 1;
};
/**
* Function: paintVertexShape
*
* Paints the swimlane vertex shape.
*/
mxSwimlane.prototype.paintVertexShape = function(c, x, y, w, h)
{
var start = this.getTitleSize();
var fill = mxUtils.getValue(this.style, mxConstants.STYLE_SWIMLANE_FILLCOLOR, mxConstants.NONE);
var swimlaneLine = mxUtils.getValue(this.style, mxConstants.STYLE_SWIMLANE_LINE, 1) == 1;
var r = 0;
if (this.isHorizontal())
{
start = Math.min(start, h);
}
else
{
start = Math.min(start, w);
}
c.translate(x, y);
if (!this.isRounded)
{
this.paintSwimlane(c, x, y, w, h, start, fill, swimlaneLine);
}
else
{
r = this.getArcSize(w, h, start);
r = Math.min(((this.isHorizontal()) ? h : w) - start, Math.min(start, r));
this.paintRoundedSwimlane(c, x, y, w, h, start, r, fill, swimlaneLine);
}
var sep = mxUtils.getValue(this.style, mxConstants.STYLE_SEPARATORCOLOR, mxConstants.NONE);
this.paintSeparator(c, x, y, w, h, start, sep);
if (this.image != null)
{
var bounds = this.getImageBounds(x, y, w, h);
c.image(bounds.x - x, bounds.y - y, bounds.width, bounds.height,
this.image, false, false, false);
}
if (this.glass)
{
c.setShadow(false);
this.paintGlassEffect(c, 0, 0, w, start, r);
}
};
/**
* Function: paintSwimlane
*
* Paints the swimlane vertex shape.
*/
mxSwimlane.prototype.paintSwimlane = function(c, x, y, w, h, start, fill, swimlaneLine)
{
c.begin();
if (this.isHorizontal())
{
c.moveTo(0, start);
c.lineTo(0, 0);
c.lineTo(w, 0);
c.lineTo(w, start);
c.fillAndStroke();
if (start < h)
{
if (fill == mxConstants.NONE)
{
c.pointerEvents = false;
}
else
{
c.setFillColor(fill);
}
c.begin();
c.moveTo(0, start);
c.lineTo(0, h);
c.lineTo(w, h);
c.lineTo(w, start);
if (fill == mxConstants.NONE)
{
c.stroke();
}
else
{
c.fillAndStroke();
}
}
}
else
{
c.moveTo(start, 0);
c.lineTo(0, 0);
c.lineTo(0, h);
c.lineTo(start, h);
c.fillAndStroke();
if (start < w)
{
if (fill == mxConstants.NONE)
{
c.pointerEvents = false;
}
else
{
c.setFillColor(fill);
}
c.begin();
c.moveTo(start, 0);
c.lineTo(w, 0);
c.lineTo(w, h);
c.lineTo(start, h);
if (fill == mxConstants.NONE)
{
c.stroke();
}
else
{
c.fillAndStroke();
}
}
}
if (swimlaneLine)
{
this.paintDivider(c, x, y, w, h, start, fill == mxConstants.NONE);
}
};
/**
* Function: paintRoundedSwimlane
*
* Paints the swimlane vertex shape.
*/
mxSwimlane.prototype.paintRoundedSwimlane = function(c, x, y, w, h, start, r, fill, swimlaneLine)
{
c.begin();
if (this.isHorizontal())
{
c.moveTo(w, start);
c.lineTo(w, r);
c.quadTo(w, 0, w - Math.min(w / 2, r), 0);
c.lineTo(Math.min(w / 2, r), 0);
c.quadTo(0, 0, 0, r);
c.lineTo(0, start);
c.fillAndStroke();
if (start < h)
{
if (fill == mxConstants.NONE)
{
c.pointerEvents = false;
}
else
{
c.setFillColor(fill);
}
c.begin();
c.moveTo(0, start);
c.lineTo(0, h - r);
c.quadTo(0, h, Math.min(w / 2, r), h);
c.lineTo(w - Math.min(w / 2, r), h);
c.quadTo(w, h, w, h - r);
c.lineTo(w, start);
if (fill == mxConstants.NONE)
{
c.stroke();
}
else
{
c.fillAndStroke();
}
}
}
else
{
c.moveTo(start, 0);
c.lineTo(r, 0);
c.quadTo(0, 0, 0, Math.min(h / 2, r));
c.lineTo(0, h - Math.min(h / 2, r));
c.quadTo(0, h, r, h);
c.lineTo(start, h);
c.fillAndStroke();
if (start < w)
{
if (fill == mxConstants.NONE)
{
c.pointerEvents = false;
}
else
{
c.setFillColor(fill);
}
c.begin();
c.moveTo(start, h);
c.lineTo(w - r, h);
c.quadTo(w, h, w, h - Math.min(h / 2, r));
c.lineTo(w, Math.min(h / 2, r));
c.quadTo(w, 0, w - r, 0);
c.lineTo(start, 0);
if (fill == mxConstants.NONE)
{
c.stroke();
}
else
{
c.fillAndStroke();
}
}
}
if (swimlaneLine)
{
this.paintDivider(c, x, y, w, h, start, fill == mxConstants.NONE);
}
};
/**
* Function: paintDivider
*
* Paints the divider between swimlane title and content area.
*/
mxSwimlane.prototype.paintDivider = function(c, x, y, w, h, start, shadow)
{
if (!shadow)
{
c.setShadow(false);
}
c.begin();
if (this.isHorizontal())
{
c.moveTo(0, start);
c.lineTo(w, start);
}
else
{
c.moveTo(start, 0);
c.lineTo(start, h);
}
c.stroke();
};
/**
* Function: paintSeparator
*
* Paints the vertical or horizontal separator line between swimlanes.
*/
mxSwimlane.prototype.paintSeparator = function(c, x, y, w, h, start, color)
{
if (color != mxConstants.NONE)
{
c.setStrokeColor(color);
c.setDashed(true);
c.begin();
if (this.isHorizontal())
{
c.moveTo(w, start);
c.lineTo(w, h);
}
else
{
c.moveTo(start, 0);
c.lineTo(w, 0);
}
c.stroke();
c.setDashed(false);
}
};
/**
* Function: getImageBounds
*
* Paints the swimlane vertex shape.
*/
mxSwimlane.prototype.getImageBounds = function(x, y, w, h)
{
if (this.isHorizontal())
{
return new mxRectangle(x + w - this.imageSize, y, this.imageSize, this.imageSize);
}
else
{
return new mxRectangle(x, y, this.imageSize, this.imageSize);
}
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxGraphLayout
*
* Base class for all layout algorithms in mxGraph. Main public functions are
* <moveCell> for handling a moved cell within a layouted parent, and <execute> for
* running the layout on a given parent cell.
*
* Known Subclasses:
*
* <mxCircleLayout>, <mxCompactTreeLayout>, <mxCompositeLayout>,
* <mxFastOrganicLayout>, <mxParallelEdgeLayout>, <mxPartitionLayout>,
* <mxStackLayout>
*
* Constructor: mxGraphLayout
*
* Constructs a new layout using the given layouts.
*
* Arguments:
*
* graph - Enclosing
*/
function mxGraphLayout(graph)
{
this.graph = graph;
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxGraphLayout.prototype.graph = null;
/**
* Variable: useBoundingBox
*
* Boolean indicating if the bounding box of the label should be used if
* its available. Default is true.
*/
mxGraphLayout.prototype.useBoundingBox = true;
/**
* Variable: parent
*
* The parent cell of the layout, if any
*/
mxGraphLayout.prototype.parent = null;
/**
* Function: moveCell
*
* Notified when a cell is being moved in a parent that has automatic
* layout to update the cell state (eg. index) so that the outcome of the
* layout will position the vertex as close to the point (x, y) as
* possible.
*
* Empty implementation.
*
* Parameters:
*
* cell - <mxCell> which has been moved.
* x - X-coordinate of the new cell location.
* y - Y-coordinate of the new cell location.
*/
mxGraphLayout.prototype.moveCell = function(cell, x, y) { };
/**
* Function: execute
*
* Executes the layout algorithm for the children of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be layed out.
*/
mxGraphLayout.prototype.execute = function(parent) { };
/**
* Function: getGraph
*
* Returns the graph that this layout operates on.
*/
mxGraphLayout.prototype.getGraph = function()
{
return this.graph;
};
/**
* Function: getConstraint
*
* Returns the constraint for the given key and cell. The optional edge and
* source arguments are used to return inbound and outgoing routing-
* constraints for the given edge and vertex. This implementation always
* returns the value for the given key in the style of the given cell.
*
* Parameters:
*
* key - Key of the constraint to be returned.
* cell - <mxCell> whose constraint should be returned.
* edge - Optional <mxCell> that represents the connection whose constraint
* should be returned. Default is null.
* source - Optional boolean that specifies if the connection is incoming
* or outgoing. Default is null.
*/
mxGraphLayout.prototype.getConstraint = function(key, cell, edge, source)
{
var state = this.graph.view.getState(cell);
var style = (state != null) ? state.style : this.graph.getCellStyle(cell);
return (style != null) ? style[key] : null;
};
/**
* Function: traverse
*
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Example:
*
* (code)
* mxLog.show();
* var cell = graph.getSelectionCell();
* graph.traverse(cell, false, function(vertex, edge)
* {
* mxLog.debug(graph.getLabel(vertex));
* });
* (end)
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - Optional boolean indicating if edges should only be traversed
* from source to target. Default is true.
* func - Visitor function that takes the current vertex and the incoming
* edge as arguments. The traversal stops if the function returns false.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* visited - Optional <mxDictionary> of cell paths for the visited cells.
*/
mxGraphLayout.traverse = function(vertex, directed, func, edge, visited)
{
if (func != null && vertex != null)
{
directed = (directed != null) ? directed : true;
visited = visited || new mxDictionary();
if (!visited.get(vertex))
{
visited.put(vertex, true);
var result = func(vertex, edge);
if (result == null || result)
{
var edgeCount = this.graph.model.getEdgeCount(vertex);
if (edgeCount > 0)
{
for (var i = 0; i < edgeCount; i++)
{
var e = this.graph.model.getEdgeAt(vertex, i);
var isSource = this.graph.model.getTerminal(e, true) == vertex;
if (!directed || isSource)
{
var next = this.graph.view.getVisibleTerminal(e, !isSource);
this.traverse(next, directed, func, e, visited);
}
}
}
}
}
}
};
/**
* Function: isAncestor
*
* Returns true if the given parent is an ancestor of the given child.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent.
* child - <mxCell> that specifies the child.
* traverseAncestors - boolean whether to
*/
mxGraphLayout.prototype.isAncestor = function(parent, child, traverseAncestors)
{
if (!traverseAncestors)
{
return (this.graph.model.getParent(child) == parent);
}
if (child == parent)
{
return false;
}
while (child != null && child != parent)
{
child = this.graph.model.getParent(child);
}
return child == parent;
};
/**
* Function: isVertexMovable
*
* Returns a boolean indicating if the given <mxCell> is movable or
* bendable by the algorithm. This implementation returns true if the given
* cell is movable in the graph.
*
* Parameters:
*
* cell - <mxCell> whose movable state should be returned.
*/
mxGraphLayout.prototype.isVertexMovable = function(cell)
{
return this.graph.isCellMovable(cell);
};
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored by
* the algorithm. This implementation returns false for all vertices.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxGraphLayout.prototype.isVertexIgnored = function(vertex)
{
return !this.graph.getModel().isVertex(vertex) ||
!this.graph.isCellVisible(vertex);
};
/**
* Function: isEdgeIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored by
* the algorithm. This implementation returns false for all vertices.
*
* Parameters:
*
* cell - <mxCell> whose ignored state should be returned.
*/
mxGraphLayout.prototype.isEdgeIgnored = function(edge)
{
var model = this.graph.getModel();
return !model.isEdge(edge) ||
!this.graph.isCellVisible(edge) ||
model.getTerminal(edge, true) == null ||
model.getTerminal(edge, false) == null;
};
/**
* Function: setEdgeStyleEnabled
*
* Disables or enables the edge style of the given edge.
*/
mxGraphLayout.prototype.setEdgeStyleEnabled = function(edge, value)
{
this.graph.setCellStyles(mxConstants.STYLE_NOEDGESTYLE,
(value) ? '0' : '1', [edge]);
};
/**
* Function: setOrthogonalEdge
*
* Disables or enables orthogonal end segments of the given edge.
*/
mxGraphLayout.prototype.setOrthogonalEdge = function(edge, value)
{
this.graph.setCellStyles(mxConstants.STYLE_ORTHOGONAL,
(value) ? '1' : '0', [edge]);
};
/**
* Function: getParentOffset
*
* Determines the offset of the given parent to the parent
* of the layout
*/
mxGraphLayout.prototype.getParentOffset = function(parent)
{
var result = new mxPoint();
if (parent != null && parent != this.parent)
{
var model = this.graph.getModel();
if (model.isAncestor(this.parent, parent))
{
var parentGeo = model.getGeometry(parent);
while (parent != this.parent)
{
result.x = result.x + parentGeo.x;
result.y = result.y + parentGeo.y;
parent = model.getParent(parent);;
parentGeo = model.getGeometry(parent);
}
}
}
return result;
};
/**
* Function: setEdgePoints
*
* Replaces the array of mxPoints in the geometry of the given edge
* with the given array of mxPoints.
*/
mxGraphLayout.prototype.setEdgePoints = function(edge, points)
{
if (edge != null)
{
var model = this.graph.model;
var geometry = model.getGeometry(edge);
if (geometry == null)
{
geometry = new mxGeometry();
geometry.setRelative(true);
}
else
{
geometry = geometry.clone();
}
if (this.parent != null && points != null)
{
var parent = model.getParent(edge);
var parentOffset = this.getParentOffset(parent);
for (var i = 0; i < points.length; i++)
{
points[i].x = points[i].x - parentOffset.x;
points[i].y = points[i].y - parentOffset.y;
}
}
geometry.points = points;
model.setGeometry(edge, geometry);
}
};
/**
* Function: setVertexLocation
*
* Sets the new position of the given cell taking into account the size of
* the bounding box if <useBoundingBox> is true. The change is only carried
* out if the new location is not equal to the existing location, otherwise
* the geometry is not replaced with an updated instance. The new or old
* bounds are returned (including overlapping labels).
*
* Parameters:
*
* cell - <mxCell> whose geometry is to be set.
* x - Integer that defines the x-coordinate of the new location.
* y - Integer that defines the y-coordinate of the new location.
*/
mxGraphLayout.prototype.setVertexLocation = function(cell, x, y)
{
var model = this.graph.getModel();
var geometry = model.getGeometry(cell);
var result = null;
if (geometry != null)
{
result = new mxRectangle(x, y, geometry.width, geometry.height);
// Checks for oversize labels and shifts the result
// TODO: Use mxUtils.getStringSize for label bounds
if (this.useBoundingBox)
{
var state = this.graph.getView().getState(cell);
if (state != null && state.text != null && state.text.boundingBox != null)
{
var scale = this.graph.getView().scale;
var box = state.text.boundingBox;
if (state.text.boundingBox.x < state.x)
{
x += (state.x - box.x) / scale;
result.width = box.width;
}
if (state.text.boundingBox.y < state.y)
{
y += (state.y - box.y) / scale;
result.height = box.height;
}
}
}
if (this.parent != null)
{
var parent = model.getParent(cell);
if (parent != null && parent != this.parent)
{
var parentOffset = this.getParentOffset(parent);
x = x - parentOffset.x;
y = y - parentOffset.y;
}
}
if (geometry.x != x || geometry.y != y)
{
geometry = geometry.clone();
geometry.x = x;
geometry.y = y;
model.setGeometry(cell, geometry);
}
}
return result;
};
/**
* Function: getVertexBounds
*
* Returns an <mxRectangle> that defines the bounds of the given cell or
* the bounding box if <useBoundingBox> is true.
*/
mxGraphLayout.prototype.getVertexBounds = function(cell)
{
var geo = this.graph.getModel().getGeometry(cell);
// Checks for oversize label bounding box and corrects
// the return value accordingly
// TODO: Use mxUtils.getStringSize for label bounds
if (this.useBoundingBox)
{
var state = this.graph.getView().getState(cell);
if (state != null && state.text != null && state.text.boundingBox != null)
{
var scale = this.graph.getView().scale;
var tmp = state.text.boundingBox;
var dx0 = Math.max(state.x - tmp.x, 0) / scale;
var dy0 = Math.max(state.y - tmp.y, 0) / scale;
var dx1 = Math.max((tmp.x + tmp.width) - (state.x + state.width), 0) / scale;
var dy1 = Math.max((tmp.y + tmp.height) - (state.y + state.height), 0) / scale;
geo = new mxRectangle(geo.x - dx0, geo.y - dy0, geo.width + dx0 + dx1, geo.height + dy0 + dy1);
}
}
if (this.parent != null)
{
var parent = this.graph.getModel().getParent(cell);
geo = geo.clone();
if (parent != null && parent != this.parent)
{
var parentOffset = this.getParentOffset(parent);
geo.x = geo.x + parentOffset.x;
geo.y = geo.y + parentOffset.y;
}
}
return new mxRectangle(geo.x, geo.y, geo.width, geo.height);
};
/**
* Function: arrangeGroups
*
* Shortcut to <mxGraph.updateGroupBounds> with moveGroup set to true.
*/
mxGraphLayout.prototype.arrangeGroups = function(cells, border, topBorder, rightBorder, bottomBorder, leftBorder)
{
return this.graph.updateGroupBounds(cells, border, true, topBorder, rightBorder, bottomBorder, leftBorder);
};
/**
* Class: WeightedCellSorter
*
* A utility class used to track cells whilst sorting occurs on the weighted
* sum of their connected edges. Does not violate (x.compareTo(y)==0) ==
* (x.equals(y))
*
* Constructor: WeightedCellSorter
*
* Constructs a new weighted cell sorted for the given cell and weight.
*/
function WeightedCellSorter(cell, weightedValue)
{
this.cell = cell;
this.weightedValue = weightedValue;
};
/**
* Variable: weightedValue
*
* The weighted value of the cell stored.
*/
WeightedCellSorter.prototype.weightedValue = 0;
/**
* Variable: nudge
*
* Whether or not to flip equal weight values.
*/
WeightedCellSorter.prototype.nudge = false;
/**
* Variable: visited
*
* Whether or not this cell has been visited in the current assignment.
*/
WeightedCellSorter.prototype.visited = false;
/**
* Variable: rankIndex
*
* The index this cell is in the model rank.
*/
WeightedCellSorter.prototype.rankIndex = null;
/**
* Variable: cell
*
* The cell whose median value is being calculated.
*/
WeightedCellSorter.prototype.cell = null;
/**
* Function: compare
*
* Compares two WeightedCellSorters.
*/
WeightedCellSorter.prototype.compare = function(a, b)
{
if (a != null && b != null)
{
if (b.weightedValue > a.weightedValue)
{
return -1;
}
else if (b.weightedValue < a.weightedValue)
{
return 1;
}
else
{
if (b.nudge)
{
return -1;
}
else
{
return 1;
}
}
}
else
{
return 0;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStackLayout
*
* Extends <mxGraphLayout> to create a horizontal or vertical stack of the
* child vertices. The children do not need to be connected for this layout
* to work.
*
* Example:
*
* (code)
* var layout = new mxStackLayout(graph, true);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxStackLayout
*
* Constructs a new stack layout layout for the specified graph,
* spacing, orientation and offset.
*/
function mxStackLayout(graph, horizontal, spacing, x0, y0, border)
{
mxGraphLayout.call(this, graph);
this.horizontal = (horizontal != null) ? horizontal : true;
this.spacing = (spacing != null) ? spacing : 0;
this.x0 = (x0 != null) ? x0 : 0;
this.y0 = (y0 != null) ? y0 : 0;
this.border = (border != null) ? border : 0;
};
/**
* Extends mxGraphLayout.
*/
mxStackLayout.prototype = new mxGraphLayout();
mxStackLayout.prototype.constructor = mxStackLayout;
/**
* Variable: horizontal
*
* Specifies the orientation of the layout. Default is true.
*/
mxStackLayout.prototype.horizontal = null;
/**
* Variable: spacing
*
* Specifies the spacing between the cells. Default is 0.
*/
mxStackLayout.prototype.spacing = null;
/**
* Variable: x0
*
* Specifies the horizontal origin of the layout. Default is 0.
*/
mxStackLayout.prototype.x0 = null;
/**
* Variable: y0
*
* Specifies the vertical origin of the layout. Default is 0.
*/
mxStackLayout.prototype.y0 = null;
/**
* Variable: border
*
* Border to be added if fill is true. Default is 0.
*/
mxStackLayout.prototype.border = 0;
/**
* Variable: marginTop
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginTop = 0;
/**
* Variable: marginLeft
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginLeft = 0;
/**
* Variable: marginRight
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginRight = 0;
/**
* Variable: marginBottom
*
* Top margin for the child area. Default is 0.
*/
mxStackLayout.prototype.marginBottom = 0;
/**
* Variable: keepFirstLocation
*
* Boolean indicating if the location of the first cell should be
* kept, that is, it will not be moved to x0 or y0. Default is false.
*/
mxStackLayout.prototype.keepFirstLocation = false;
/**
* Variable: fill
*
* Boolean indicating if dimension should be changed to fill out the parent
* cell. Default is false.
*/
mxStackLayout.prototype.fill = false;
/**
* Variable: resizeParent
*
* If the parent should be resized to match the width/height of the
* stack. Default is false.
*/
mxStackLayout.prototype.resizeParent = false;
/**
* Variable: resizeParentMax
*
* Use maximum of existing value and new value for resize of parent.
* Default is false.
*/
mxStackLayout.prototype.resizeParentMax = false;
/**
* Variable: resizeLast
*
* If the last element should be resized to fill out the parent. Default is
* false. If <resizeParent> is true then this is ignored.
*/
mxStackLayout.prototype.resizeLast = false;
/**
* Variable: wrap
*
* Value at which a new column or row should be created. Default is null.
*/
mxStackLayout.prototype.wrap = null;
/**
* Variable: borderCollapse
*
* If the strokeWidth should be ignored. Default is true.
*/
mxStackLayout.prototype.borderCollapse = true;
/**
* Variable: allowGaps
*
* If gaps should be allowed in the stack. Default is false.
*/
mxStackLayout.prototype.allowGaps = false;
/**
* Variable: gridSize
*
* Grid size for alignment of position and size. Default is 0.
*/
mxStackLayout.prototype.gridSize = 0;
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxStackLayout.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell>.
*/
mxStackLayout.prototype.moveCell = function(cell, x, y)
{
var model = this.graph.getModel();
var parent = model.getParent(cell);
var horizontal = this.isHorizontal();
if (cell != null && parent != null)
{
var i = 0;
var last = 0;
var childCount = model.getChildCount(parent);
var value = (horizontal) ? x : y;
var pstate = this.graph.getView().getState(parent);
if (pstate != null)
{
value -= (horizontal) ? pstate.x : pstate.y;
}
value /= this.graph.view.scale;
for (i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (child != cell)
{
var bounds = model.getGeometry(child);
if (bounds != null)
{
var tmp = (horizontal) ?
bounds.x + bounds.width / 2 :
bounds.y + bounds.height / 2;
if (last <= value && tmp > value)
{
break;
}
last = tmp;
}
}
}
// Changes child order in parent
var idx = parent.getIndex(cell);
idx = Math.max(0, i - ((i > idx) ? 1 : 0));
model.add(parent, cell, idx);
}
};
/**
* Function: getParentSize
*
* Returns the size for the parent container or the size of the graph
* container if the parent is a layer or the root of the model.
*/
mxStackLayout.prototype.getParentSize = function(parent)
{
var model = this.graph.getModel();
var pgeo = model.getGeometry(parent);
// Handles special case where the parent is either a layer with no
// geometry or the current root of the view in which case the size
// of the graph's container will be used.
if (this.graph.container != null && ((pgeo == null &&
model.isLayer(parent)) || parent == this.graph.getView().currentRoot))
{
var width = this.graph.container.offsetWidth - 1;
var height = this.graph.container.offsetHeight - 1;
pgeo = new mxRectangle(0, 0, width, height);
}
return pgeo;
};
/**
* Function: getLayoutCells
*
* Returns the cells to be layouted.
*/
mxStackLayout.prototype.getLayoutCells = function(parent)
{
var model = this.graph.getModel();
var childCount = model.getChildCount(parent);
var cells = [];
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (!this.isVertexIgnored(child) && this.isVertexMovable(child))
{
cells.push(child);
}
}
if (this.allowGaps)
{
cells.sort(mxUtils.bind(this, function(c1, c2)
{
var geo1 = this.graph.getCellGeometry(c1);
var geo2 = this.graph.getCellGeometry(c2);
return (geo1.y == geo2.y) ? 0 : ((geo1.y > geo2.y > 0) ? 1 : -1);
}));
}
return cells;
};
/**
* Function: snap
*
* Snaps the given value to the grid size.
*/
mxStackLayout.prototype.snap = function(value)
{
if (this.gridSize != null && this.gridSize > 0)
{
value = Math.max(value, this.gridSize);
if (value / this.gridSize > 1)
{
var mod = value % this.gridSize;
value += mod > this.gridSize / 2 ? (this.gridSize - mod) : -mod;
}
}
return value;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* Only children where <isVertexIgnored> returns false are taken into
* account.
*/
mxStackLayout.prototype.execute = function(parent)
{
if (parent != null)
{
var pgeo = this.getParentSize(parent);
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var fillValue = null;
if (pgeo != null)
{
fillValue = (horizontal) ? pgeo.height - this.marginTop - this.marginBottom :
pgeo.width - this.marginLeft - this.marginRight;
}
fillValue -= 2 * this.border;
var x0 = this.x0 + this.border + this.marginLeft;
var y0 = this.y0 + this.border + this.marginTop;
// Handles swimlane start size
if (this.graph.isSwimlane(parent))
{
// Uses computed style to get latest
var style = this.graph.getCellStyle(parent);
var start = mxUtils.getNumber(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE);
var horz = mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true) == 1;
if (pgeo != null)
{
if (horz)
{
start = Math.min(start, pgeo.height);
}
else
{
start = Math.min(start, pgeo.width);
}
}
if (horizontal == horz)
{
fillValue -= start;
}
if (horz)
{
y0 += start;
}
else
{
x0 += start;
}
}
model.beginUpdate();
try
{
var tmp = 0;
var last = null;
var lastValue = 0;
var lastChild = null;
var cells = this.getLayoutCells(parent);
for (var i = 0; i < cells.length; i++)
{
var child = cells[i];
var geo = model.getGeometry(child);
if (geo != null)
{
geo = geo.clone();
if (this.wrap != null && last != null)
{
if ((horizontal && last.x + last.width +
geo.width + 2 * this.spacing > this.wrap) ||
(!horizontal && last.y + last.height +
geo.height + 2 * this.spacing > this.wrap))
{
last = null;
if (horizontal)
{
y0 += tmp + this.spacing;
}
else
{
x0 += tmp + this.spacing;
}
tmp = 0;
}
}
tmp = Math.max(tmp, (horizontal) ? geo.height : geo.width);
var sw = 0;
if (!this.borderCollapse)
{
var childStyle = this.graph.getCellStyle(child);
sw = mxUtils.getNumber(childStyle, mxConstants.STYLE_STROKEWIDTH, 1);
}
if (last != null)
{
var temp = lastValue + this.spacing + Math.floor(sw / 2);
if (horizontal)
{
geo.x = this.snap(((this.allowGaps) ? Math.max(temp, geo.x) :
temp) - this.marginLeft) + this.marginLeft;
}
else
{
geo.y = this.snap(((this.allowGaps) ? Math.max(temp, geo.y) :
temp) - this.marginTop) + this.marginTop;
}
}
else if (!this.keepFirstLocation)
{
if (horizontal)
{
geo.x = (this.allowGaps && geo.x > x0) ? Math.max(this.snap(geo.x -
this.marginLeft) + this.marginLeft, x0) : x0;
}
else
{
geo.y = (this.allowGaps && geo.y > y0) ? Math.max(this.snap(geo.y -
this.marginTop) + this.marginTop, y0) : y0;
}
}
if (horizontal)
{
geo.y = y0;
}
else
{
geo.x = x0;
}
if (this.fill && fillValue != null)
{
if (horizontal)
{
geo.height = fillValue;
}
else
{
geo.width = fillValue;
}
}
if (horizontal)
{
geo.width = this.snap(geo.width);
}
else
{
geo.height = this.snap(geo.height);
}
this.setChildGeometry(child, geo);
lastChild = child;
last = geo;
if (horizontal)
{
lastValue = last.x + last.width + Math.floor(sw / 2);
}
else
{
lastValue = last.y + last.height + Math.floor(sw / 2);
}
}
}
if (this.resizeParent && pgeo != null && last != null && !this.graph.isCellCollapsed(parent))
{
this.updateParentGeometry(parent, pgeo, last);
}
else if (this.resizeLast && pgeo != null && last != null && lastChild != null)
{
if (horizontal)
{
last.width = pgeo.width - last.x - this.spacing - this.marginRight - this.marginLeft;
}
else
{
last.height = pgeo.height - last.y - this.spacing - this.marginBottom;
}
this.setChildGeometry(lastChild, last);
}
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* Only children where <isVertexIgnored> returns false are taken into
* account.
*/
mxStackLayout.prototype.setChildGeometry = function(child, geo)
{
var geo2 = this.graph.getCellGeometry(child);
if (geo2 == null || geo.x != geo2.x || geo.y != geo2.y ||
geo.width != geo2.width || geo.height != geo2.height)
{
this.graph.getModel().setGeometry(child, geo);
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* Only children where <isVertexIgnored> returns false are taken into
* account.
*/
mxStackLayout.prototype.updateParentGeometry = function(parent, pgeo, last)
{
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var pgeo2 = pgeo.clone();
if (horizontal)
{
var tmp = last.x + last.width + this.marginRight + this.border;
if (this.resizeParentMax)
{
pgeo2.width = Math.max(pgeo2.width, tmp);
}
else
{
pgeo2.width = tmp;
}
}
else
{
var tmp = last.y + last.height + this.marginBottom + this.border;
if (this.resizeParentMax)
{
pgeo2.height = Math.max(pgeo2.height, tmp);
}
else
{
pgeo2.height = tmp;
}
}
if (pgeo.x != pgeo2.x || pgeo.y != pgeo2.y ||
pgeo.width != pgeo2.width || pgeo.height != pgeo2.height)
{
model.setGeometry(parent, pgeo2);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPartitionLayout
*
* Extends <mxGraphLayout> for partitioning the parent cell vertically or
* horizontally by filling the complete area with the child cells. A horizontal
* layout partitions the height of the given parent whereas a a non-horizontal
* layout partitions the width. If the parent is a layer (that is, a child of
* the root node), then the current graph size is partitioned. The children do
* not need to be connected for this layout to work.
*
* Example:
*
* (code)
* var layout = new mxPartitionLayout(graph, true, 10, 20);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxPartitionLayout
*
* Constructs a new stack layout layout for the specified graph,
* spacing, orientation and offset.
*/
function mxPartitionLayout(graph, horizontal, spacing, border)
{
mxGraphLayout.call(this, graph);
this.horizontal = (horizontal != null) ? horizontal : true;
this.spacing = spacing || 0;
this.border = border || 0;
};
/**
* Extends mxGraphLayout.
*/
mxPartitionLayout.prototype = new mxGraphLayout();
mxPartitionLayout.prototype.constructor = mxPartitionLayout;
/**
* Variable: horizontal
*
* Boolean indicating the direction in which the space is partitioned.
* Default is true.
*/
mxPartitionLayout.prototype.horizontal = null;
/**
* Variable: spacing
*
* Integer that specifies the absolute spacing in pixels between the
* children. Default is 0.
*/
mxPartitionLayout.prototype.spacing = null;
/**
* Variable: border
*
* Integer that specifies the absolute inset in pixels for the parent that
* contains the children. Default is 0.
*/
mxPartitionLayout.prototype.border = null;
/**
* Variable: resizeVertices
*
* Boolean that specifies if vertices should be resized. Default is true.
*/
mxPartitionLayout.prototype.resizeVertices = true;
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxPartitionLayout.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell>.
*/
mxPartitionLayout.prototype.moveCell = function(cell, x, y)
{
var model = this.graph.getModel();
var parent = model.getParent(cell);
if (cell != null &&
parent != null)
{
var i = 0;
var last = 0;
var childCount = model.getChildCount(parent);
// Finds index of the closest swimlane
// TODO: Take into account the orientation
for (i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
var bounds = this.getVertexBounds(child);
if (bounds != null)
{
var tmp = bounds.x + bounds.width / 2;
if (last < x && tmp > x)
{
break;
}
last = tmp;
}
}
// Changes child order in parent
var idx = parent.getIndex(cell);
idx = Math.max(0, i - ((i > idx) ? 1 : 0));
model.add(parent, cell, idx);
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>. All children where <isVertexIgnored>
* returns false and <isVertexMovable> returns true are modified.
*/
mxPartitionLayout.prototype.execute = function(parent)
{
var horizontal = this.isHorizontal();
var model = this.graph.getModel();
var pgeo = model.getGeometry(parent);
// Handles special case where the parent is either a layer with no
// geometry or the current root of the view in which case the size
// of the graph's container will be used.
if (this.graph.container != null &&
((pgeo == null &&
model.isLayer(parent)) ||
parent == this.graph.getView().currentRoot))
{
var width = this.graph.container.offsetWidth - 1;
var height = this.graph.container.offsetHeight - 1;
pgeo = new mxRectangle(0, 0, width, height);
}
if (pgeo != null)
{
var children = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (!this.isVertexIgnored(child) &&
this.isVertexMovable(child))
{
children.push(child);
}
}
var n = children.length;
if (n > 0)
{
var x0 = this.border;
var y0 = this.border;
var other = (horizontal) ? pgeo.height : pgeo.width;
other -= 2 * this.border;
var size = (this.graph.isSwimlane(parent)) ?
this.graph.getStartSize(parent) :
new mxRectangle();
other -= (horizontal) ? size.height : size.width;
x0 = x0 + size.width;
y0 = y0 + size.height;
var tmp = this.border + (n - 1) * this.spacing;
var value = (horizontal) ?
((pgeo.width - x0 - tmp) / n) :
((pgeo.height - y0 - tmp) / n);
// Avoids negative values, that is values where the sum of the
// spacing plus the border is larger then the available space
if (value > 0)
{
model.beginUpdate();
try
{
for (var i = 0; i < n; i++)
{
var child = children[i];
var geo = model.getGeometry(child);
if (geo != null)
{
geo = geo.clone();
geo.x = x0;
geo.y = y0;
if (horizontal)
{
if (this.resizeVertices)
{
geo.width = value;
geo.height = other;
}
x0 += value + this.spacing;
}
else
{
if (this.resizeVertices)
{
geo.height = value;
geo.width = other;
}
y0 += value + this.spacing;
}
model.setGeometry(child, geo);
}
}
}
finally
{
model.endUpdate();
}
}
}
}
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxCompactTreeLayout
*
* Extends <mxGraphLayout> to implement a compact tree (Moen) algorithm. This
* layout is suitable for graphs that have no cycles (trees). Vertices that are
* not connected to the tree will be ignored by this layout.
*
* Example:
*
* (code)
* var layout = new mxCompactTreeLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCompactTreeLayout
*
* Constructs a new compact tree layout for the specified graph
* and orientation.
*/
function mxCompactTreeLayout(graph, horizontal, invert)
{
mxGraphLayout.call(this, graph);
this.horizontal = (horizontal != null) ? horizontal : true;
this.invert = (invert != null) ? invert : false;
};
/**
* Extends mxGraphLayout.
*/
mxCompactTreeLayout.prototype = new mxGraphLayout();
mxCompactTreeLayout.prototype.constructor = mxCompactTreeLayout;
/**
* Variable: horizontal
*
* Specifies the orientation of the layout. Default is true.
*/
mxCompactTreeLayout.prototype.horizontal = null;
/**
* Variable: invert
*
* Specifies if edge directions should be inverted. Default is false.
*/
mxCompactTreeLayout.prototype.invert = null;
/**
* Variable: resizeParent
*
* If the parents should be resized to match the width/height of the
* children. Default is true.
*/
mxCompactTreeLayout.prototype.resizeParent = true;
/**
* Variable: maintainParentLocation
*
* Specifies if the parent location should be maintained, so that the
* top, left corner stays the same before and after execution of
* the layout. Default is false for backwards compatibility.
*/
mxCompactTreeLayout.prototype.maintainParentLocation = false;
/**
* Variable: groupPadding
*
* Padding added to resized parents. Default is 10.
*/
mxCompactTreeLayout.prototype.groupPadding = 10;
/**
* Variable: groupPaddingTop
*
* Top padding added to resized parents. Default is 0.
*/
mxCompactTreeLayout.prototype.groupPaddingTop = 0;
/**
* Variable: groupPaddingRight
*
* Right padding added to resized parents. Default is 0.
*/
mxCompactTreeLayout.prototype.groupPaddingRight = 0;
/**
* Variable: groupPaddingBottom
*
* Bottom padding added to resized parents. Default is 0.
*/
mxCompactTreeLayout.prototype.groupPaddingBottom = 0;
/**
* Variable: groupPaddingLeft
*
* Left padding added to resized parents. Default is 0.
*/
mxCompactTreeLayout.prototype.groupPaddingLeft = 0;
/**
* Variable: parentsChanged
*
* A set of the parents that need updating based on children
* process as part of the layout.
*/
mxCompactTreeLayout.prototype.parentsChanged = null;
/**
* Variable: moveTree
*
* Specifies if the tree should be moved to the top, left corner
* if it is inside a top-level layer. Default is false.
*/
mxCompactTreeLayout.prototype.moveTree = false;
/**
* Variable: visited
*
* Specifies if the tree should be moved to the top, left corner
* if it is inside a top-level layer. Default is false.
*/
mxCompactTreeLayout.prototype.visited = null;
/**
* Variable: levelDistance
*
* Holds the levelDistance. Default is 10.
*/
mxCompactTreeLayout.prototype.levelDistance = 10;
/**
* Variable: nodeDistance
*
* Holds the nodeDistance. Default is 20.
*/
mxCompactTreeLayout.prototype.nodeDistance = 20;
/**
* Variable: resetEdges
*
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
mxCompactTreeLayout.prototype.resetEdges = true;
/**
* Variable: prefHozEdgeSep
*
* The preferred horizontal distance between edges exiting a vertex.
*/
mxCompactTreeLayout.prototype.prefHozEdgeSep = 5;
/**
* Variable: prefVertEdgeOff
*
* The preferred vertical offset between edges exiting a vertex.
*/
mxCompactTreeLayout.prototype.prefVertEdgeOff = 4;
/**
* Variable: minEdgeJetty
*
* The minimum distance for an edge jetty from a vertex.
*/
mxCompactTreeLayout.prototype.minEdgeJetty = 8;
/**
* Variable: channelBuffer
*
* The size of the vertical buffer in the center of inter-rank channels
* where edge control points should not be placed.
*/
mxCompactTreeLayout.prototype.channelBuffer = 4;
/**
* Variable: edgeRouting
*
* Whether or not to apply the internal tree edge routing.
*/
mxCompactTreeLayout.prototype.edgeRouting = true;
/**
* Variable: sortEdges
*
* Specifies if edges should be sorted according to the order of their
* opposite terminal cell in the model.
*/
mxCompactTreeLayout.prototype.sortEdges = false;
/**
* Variable: alignRanks
*
* Whether or not the tops of cells in each rank should be aligned
* across the rank
*/
mxCompactTreeLayout.prototype.alignRanks = false;
/**
* Variable: maxRankHeight
*
* An array of the maximum height of cells (relative to the layout direction)
* per rank
*/
mxCompactTreeLayout.prototype.maxRankHeight = null;
/**
* Variable: root
*
* The cell to use as the root of the tree
*/
mxCompactTreeLayout.prototype.root = null;
/**
* Variable: node
*
* The internal node representation of the root cell. Do not set directly
* , this value is only exposed to assist with post-processing functionality
*/
mxCompactTreeLayout.prototype.node = null;
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxCompactTreeLayout.prototype.isVertexIgnored = function(vertex)
{
return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
this.graph.getConnections(vertex).length == 0;
};
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxCompactTreeLayout.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* If the parent has any connected edges, then it is used as the root of
* the tree. Else, <mxGraph.findTreeRoots> will be used to find a suitable
* root node within the set of children of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be laid out.
* root - Optional <mxCell> that will be used as the root of the tree.
* Overrides <root> if specified.
*/
mxCompactTreeLayout.prototype.execute = function(parent, root)
{
this.parent = parent;
var model = this.graph.getModel();
if (root == null)
{
// Takes the parent as the root if it has outgoing edges
if (this.graph.getEdges(parent, model.getParent(parent),
this.invert, !this.invert, false).length > 0)
{
this.root = parent;
}
// Tries to find a suitable root in the parent's
// children
else
{
var roots = this.graph.findTreeRoots(parent, true, this.invert);
if (roots.length > 0)
{
for (var i = 0; i < roots.length; i++)
{
if (!this.isVertexIgnored(roots[i]) &&
this.graph.getEdges(roots[i], null,
this.invert, !this.invert, false).length > 0)
{
this.root = roots[i];
break;
}
}
}
}
}
else
{
this.root = root;
}
if (this.root != null)
{
if (this.resizeParent)
{
this.parentsChanged = new Object();
}
else
{
this.parentsChanged = null;
}
// Maintaining parent location
this.parentX = null;
this.parentY = null;
if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
this.parentX = geo.x;
this.parentY = geo.y;
}
}
model.beginUpdate();
try
{
this.visited = new Object();
this.node = this.dfs(this.root, parent);
if (this.alignRanks)
{
this.maxRankHeight = [];
this.findRankHeights(this.node, 0);
this.setCellHeights(this.node, 0);
}
if (this.node != null)
{
this.layout(this.node);
var x0 = this.graph.gridSize;
var y0 = x0;
if (!this.moveTree)
{
var g = this.getVertexBounds(this.root);
if (g != null)
{
x0 = g.x;
y0 = g.y;
}
}
var bounds = null;
if (this.isHorizontal())
{
bounds = this.horizontalLayout(this.node, x0, y0);
}
else
{
bounds = this.verticalLayout(this.node, null, x0, y0);
}
if (bounds != null)
{
var dx = 0;
var dy = 0;
if (bounds.x < 0)
{
dx = Math.abs(x0 - bounds.x);
}
if (bounds.y < 0)
{
dy = Math.abs(y0 - bounds.y);
}
if (dx != 0 || dy != 0)
{
this.moveNode(this.node, dx, dy);
}
if (this.resizeParent)
{
this.adjustParents();
}
if (this.edgeRouting)
{
// Iterate through all edges setting their positions
this.localEdgeProcessing(this.node);
}
}
// Maintaining parent location
if (this.parentX != null && this.parentY != null)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
geo = geo.clone();
geo.x = this.parentX;
geo.y = this.parentY;
model.setGeometry(parent, geo);
}
}
}
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: moveNode
*
* Moves the specified node and all of its children by the given amount.
*/
mxCompactTreeLayout.prototype.moveNode = function(node, dx, dy)
{
node.x += dx;
node.y += dy;
this.apply(node);
var child = node.child;
while (child != null)
{
this.moveNode(child, dx, dy);
child = child.next;
}
};
/**
* Function: sortOutgoingEdges
*
* Called if <sortEdges> is true to sort the array of outgoing edges in place.
*/
mxCompactTreeLayout.prototype.sortOutgoingEdges = function(source, edges)
{
var lookup = new mxDictionary();
edges.sort(function(e1, e2)
{
var end1 = e1.getTerminal(e1.getTerminal(false) == source);
var p1 = lookup.get(end1);
if (p1 == null)
{
p1 = mxCellPath.create(end1).split(mxCellPath.PATH_SEPARATOR);
lookup.put(end1, p1);
}
var end2 = e2.getTerminal(e2.getTerminal(false) == source);
var p2 = lookup.get(end2);
if (p2 == null)
{
p2 = mxCellPath.create(end2).split(mxCellPath.PATH_SEPARATOR);
lookup.put(end2, p2);
}
return mxCellPath.compare(p1, p2);
});
};
/**
* Function: findRankHeights
*
* Stores the maximum height (relative to the layout
* direction) of cells in each rank
*/
mxCompactTreeLayout.prototype.findRankHeights = function(node, rank)
{
if (this.maxRankHeight[rank] == null || this.maxRankHeight[rank] < node.height)
{
this.maxRankHeight[rank] = node.height;
}
var child = node.child;
while (child != null)
{
this.findRankHeights(child, rank + 1);
child = child.next;
}
};
/**
* Function: setCellHeights
*
* Set the cells heights (relative to the layout
* direction) when the tops of each rank are to be aligned
*/
mxCompactTreeLayout.prototype.setCellHeights = function(node, rank)
{
if (this.maxRankHeight[rank] != null && this.maxRankHeight[rank] > node.height)
{
node.height = this.maxRankHeight[rank];
}
var child = node.child;
while (child != null)
{
this.setCellHeights(child, rank + 1);
child = child.next;
}
};
/**
* Function: dfs
*
* Does a depth first search starting at the specified cell.
* Makes sure the specified parent is never left by the
* algorithm.
*/
mxCompactTreeLayout.prototype.dfs = function(cell, parent)
{
var id = mxCellPath.create(cell);
var node = null;
if (cell != null && this.visited[id] == null && !this.isVertexIgnored(cell))
{
this.visited[id] = cell;
node = this.createNode(cell);
var model = this.graph.getModel();
var prev = null;
var out = this.graph.getEdges(cell, parent, this.invert, !this.invert, false, true);
var view = this.graph.getView();
if (this.sortEdges)
{
this.sortOutgoingEdges(cell, out);
}
for (var i = 0; i < out.length; i++)
{
var edge = out[i];
if (!this.isEdgeIgnored(edge))
{
// Resets the points on the traversed edge
if (this.resetEdges)
{
this.setEdgePoints(edge, null);
}
if (this.edgeRouting)
{
this.setEdgeStyleEnabled(edge, false);
this.setEdgePoints(edge, null);
}
// Checks if terminal in same swimlane
var state = view.getState(edge);
var target = (state != null) ? state.getVisibleTerminal(this.invert) : view.getVisibleTerminal(edge, this.invert);
var tmp = this.dfs(target, parent);
if (tmp != null && model.getGeometry(target) != null)
{
if (prev == null)
{
node.child = tmp;
}
else
{
prev.next = tmp;
}
prev = tmp;
}
}
}
}
return node;
};
/**
* Function: layout
*
* Starts the actual compact tree layout algorithm
* at the given node.
*/
mxCompactTreeLayout.prototype.layout = function(node)
{
if (node != null)
{
var child = node.child;
while (child != null)
{
this.layout(child);
child = child.next;
}
if (node.child != null)
{
this.attachParent(node, this.join(node));
}
else
{
this.layoutLeaf(node);
}
}
};
/**
* Function: horizontalLayout
*/
mxCompactTreeLayout.prototype.horizontalLayout = function(node, x0, y0, bounds)
{
node.x += x0 + node.offsetX;
node.y += y0 + node.offsetY;
bounds = this.apply(node, bounds);
var child = node.child;
if (child != null)
{
bounds = this.horizontalLayout(child, node.x, node.y, bounds);
var siblingOffset = node.y + child.offsetY;
var s = child.next;
while (s != null)
{
bounds = this.horizontalLayout(s, node.x + child.offsetX, siblingOffset, bounds);
siblingOffset += s.offsetY;
s = s.next;
}
}
return bounds;
};
/**
* Function: verticalLayout
*/
mxCompactTreeLayout.prototype.verticalLayout = function(node, parent, x0, y0, bounds)
{
node.x += x0 + node.offsetY;
node.y += y0 + node.offsetX;
bounds = this.apply(node, bounds);
var child = node.child;
if (child != null)
{
bounds = this.verticalLayout(child, node, node.x, node.y, bounds);
var siblingOffset = node.x + child.offsetY;
var s = child.next;
while (s != null)
{
bounds = this.verticalLayout(s, node, siblingOffset, node.y + child.offsetX, bounds);
siblingOffset += s.offsetY;
s = s.next;
}
}
return bounds;
};
/**
* Function: attachParent
*/
mxCompactTreeLayout.prototype.attachParent = function(node, height)
{
var x = this.nodeDistance + this.levelDistance;
var y2 = (height - node.width) / 2 - this.nodeDistance;
var y1 = y2 + node.width + 2 * this.nodeDistance - height;
node.child.offsetX = x + node.height;
node.child.offsetY = y1;
node.contour.upperHead = this.createLine(node.height, 0,
this.createLine(x, y1, node.contour.upperHead));
node.contour.lowerHead = this.createLine(node.height, 0,
this.createLine(x, y2, node.contour.lowerHead));
};
/**
* Function: layoutLeaf
*/
mxCompactTreeLayout.prototype.layoutLeaf = function(node)
{
var dist = 2 * this.nodeDistance;
node.contour.upperTail = this.createLine(
node.height + dist, 0);
node.contour.upperHead = node.contour.upperTail;
node.contour.lowerTail = this.createLine(
0, -node.width - dist);
node.contour.lowerHead = this.createLine(
node.height + dist, 0, node.contour.lowerTail);
};
/**
* Function: join
*/
mxCompactTreeLayout.prototype.join = function(node)
{
var dist = 2 * this.nodeDistance;
var child = node.child;
node.contour = child.contour;
var h = child.width + dist;
var sum = h;
child = child.next;
while (child != null)
{
var d = this.merge(node.contour, child.contour);
child.offsetY = d + h;
child.offsetX = 0;
h = child.width + dist;
sum += d + h;
child = child.next;
}
return sum;
};
/**
* Function: merge
*/
mxCompactTreeLayout.prototype.merge = function(p1, p2)
{
var x = 0;
var y = 0;
var total = 0;
var upper = p1.lowerHead;
var lower = p2.upperHead;
while (lower != null && upper != null)
{
var d = this.offset(x, y, lower.dx, lower.dy,
upper.dx, upper.dy);
y += d;
total += d;
if (x + lower.dx <= upper.dx)
{
x += lower.dx;
y += lower.dy;
lower = lower.next;
}
else
{
x -= upper.dx;
y -= upper.dy;
upper = upper.next;
}
}
if (lower != null)
{
var b = this.bridge(p1.upperTail, 0, 0, lower, x, y);
p1.upperTail = (b.next != null) ? p2.upperTail : b;
p1.lowerTail = p2.lowerTail;
}
else
{
var b = this.bridge(p2.lowerTail, x, y, upper, 0, 0);
if (b.next == null)
{
p1.lowerTail = b;
}
}
p1.lowerHead = p2.lowerHead;
return total;
};
/**
* Function: offset
*/
mxCompactTreeLayout.prototype.offset = function(p1, p2, a1, a2, b1, b2)
{
var d = 0;
if (b1 <= p1 || p1 + a1 <= 0)
{
return 0;
}
var t = b1 * a2 - a1 * b2;
if (t > 0)
{
if (p1 < 0)
{
var s = p1 * a2;
d = s / a1 - p2;
}
else if (p1 > 0)
{
var s = p1 * b2;
d = s / b1 - p2;
}
else
{
d = -p2;
}
}
else if (b1 < p1 + a1)
{
var s = (b1 - p1) * a2;
d = b2 - (p2 + s / a1);
}
else if (b1 > p1 + a1)
{
var s = (a1 + p1) * b2;
d = s / b1 - (p2 + a2);
}
else
{
d = b2 - (p2 + a2);
}
if (d > 0)
{
return d;
}
else
{
return 0;
}
};
/**
* Function: bridge
*/
mxCompactTreeLayout.prototype.bridge = function(line1, x1, y1, line2, x2, y2)
{
var dx = x2 + line2.dx - x1;
var dy = 0;
var s = 0;
if (line2.dx == 0)
{
dy = line2.dy;
}
else
{
s = dx * line2.dy;
dy = s / line2.dx;
}
var r = this.createLine(dx, dy, line2.next);
line1.next = this.createLine(0, y2 + line2.dy - dy - y1, r);
return r;
};
/**
* Function: createNode
*/
mxCompactTreeLayout.prototype.createNode = function(cell)
{
var node = new Object();
node.cell = cell;
node.x = 0;
node.y = 0;
node.width = 0;
node.height = 0;
var geo = this.getVertexBounds(cell);
if (geo != null)
{
if (this.isHorizontal())
{
node.width = geo.height;
node.height = geo.width;
}
else
{
node.width = geo.width;
node.height = geo.height;
}
}
node.offsetX = 0;
node.offsetY = 0;
node.contour = new Object();
return node;
};
/**
* Function: apply
*/
mxCompactTreeLayout.prototype.apply = function(node, bounds)
{
var model = this.graph.getModel();
var cell = node.cell;
var g = model.getGeometry(cell);
if (cell != null && g != null)
{
if (this.isVertexMovable(cell))
{
g = this.setVertexLocation(cell, node.x, node.y);
if (this.resizeParent)
{
var parent = model.getParent(cell);
var id = mxCellPath.create(parent);
// Implements set semantic
if (this.parentsChanged[id] == null)
{
this.parentsChanged[id] = parent;
}
}
}
if (bounds == null)
{
bounds = new mxRectangle(g.x, g.y, g.width, g.height);
}
else
{
bounds = new mxRectangle(Math.min(bounds.x, g.x),
Math.min(bounds.y, g.y),
Math.max(bounds.x + bounds.width, g.x + g.width),
Math.max(bounds.y + bounds.height, g.y + g.height));
}
}
return bounds;
};
/**
* Function: createLine
*/
mxCompactTreeLayout.prototype.createLine = function(dx, dy, next)
{
var line = new Object();
line.dx = dx;
line.dy = dy;
line.next = next;
return line;
};
/**
* Function: adjustParents
*
* Adjust parent cells whose child geometries have changed. The default
* implementation adjusts the group to just fit around the children with
* a padding.
*/
mxCompactTreeLayout.prototype.adjustParents = function()
{
var tmp = [];
for (var id in this.parentsChanged)
{
tmp.push(this.parentsChanged[id]);
}
this.arrangeGroups(mxUtils.sortCells(tmp, true), this.groupPadding, this.groupPaddingTop,
this.groupPaddingRight, this.groupPaddingBottom, this.groupPaddingLeft);
};
/**
* Function: localEdgeProcessing
*
* Moves the specified node and all of its children by the given amount.
*/
mxCompactTreeLayout.prototype.localEdgeProcessing = function(node)
{
this.processNodeOutgoing(node);
var child = node.child;
while (child != null)
{
this.localEdgeProcessing(child);
child = child.next;
}
};
/**
* Function: localEdgeProcessing
*
* Separates the x position of edges as they connect to vertices
*/
mxCompactTreeLayout.prototype.processNodeOutgoing = function(node)
{
var child = node.child;
var parentCell = node.cell;
var childCount = 0;
var sortedCells = [];
while (child != null)
{
childCount++;
var sortingCriterion = child.x;
if (this.horizontal)
{
sortingCriterion = child.y;
}
sortedCells.push(new WeightedCellSorter(child, sortingCriterion));
child = child.next;
}
sortedCells.sort(WeightedCellSorter.prototype.compare);
var availableWidth = node.width;
var requiredWidth = (childCount + 1) * this.prefHozEdgeSep;
// Add a buffer on the edges of the vertex if the edge count allows
if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep))
{
availableWidth -= 2 * this.prefHozEdgeSep;
}
var edgeSpacing = availableWidth / childCount;
var currentXOffset = edgeSpacing / 2.0;
if (availableWidth > requiredWidth + (2 * this.prefHozEdgeSep))
{
currentXOffset += this.prefHozEdgeSep;
}
var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff;
var maxYOffset = 0;
var parentBounds = this.getVertexBounds(parentCell);
child = node.child;
for (var j = 0; j < sortedCells.length; j++)
{
var childCell = sortedCells[j].cell.cell;
var childBounds = this.getVertexBounds(childCell);
var edges = this.graph.getEdgesBetween(parentCell,
childCell, false);
var newPoints = [];
var x = 0;
var y = 0;
for (var i = 0; i < edges.length; i++)
{
if (this.horizontal)
{
// Use opposite co-ords, calculation was done for
//
x = parentBounds.x + parentBounds.width;
y = parentBounds.y + currentXOffset;
newPoints.push(new mxPoint(x, y));
x = parentBounds.x + parentBounds.width
+ currentYOffset;
newPoints.push(new mxPoint(x, y));
y = childBounds.y + childBounds.height / 2.0;
newPoints.push(new mxPoint(x, y));
this.setEdgePoints(edges[i], newPoints);
}
else
{
x = parentBounds.x + currentXOffset;
y = parentBounds.y + parentBounds.height;
newPoints.push(new mxPoint(x, y));
y = parentBounds.y + parentBounds.height
+ currentYOffset;
newPoints.push(new mxPoint(x, y));
x = childBounds.x + childBounds.width / 2.0;
newPoints.push(new mxPoint(x, y));
this.setEdgePoints(edges[i], newPoints);
}
}
if (j < childCount / 2)
{
currentYOffset += this.prefVertEdgeOff;
}
else if (j > childCount / 2)
{
currentYOffset -= this.prefVertEdgeOff;
}
// Ignore the case if equals, this means the second of 2
// jettys with the same y (even number of edges)
// pos[k * 2] = currentX;
currentXOffset += edgeSpacing;
// pos[k * 2 + 1] = currentYOffset;
maxYOffset = Math.max(maxYOffset, currentYOffset);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxRadialTreeLayout
*
* Extends <mxGraphLayout> to implement a radial tree algorithm. This
* layout is suitable for graphs that have no cycles (trees). Vertices that are
* not connected to the tree will be ignored by this layout.
*
* Example:
*
* (code)
* var layout = new mxRadialTreeLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxRadialTreeLayout
*
* Constructs a new radial tree layout for the specified graph
*/
function mxRadialTreeLayout(graph)
{
mxCompactTreeLayout.call(this, graph , false);
};
/**
* Extends mxGraphLayout.
*/
mxUtils.extend(mxRadialTreeLayout, mxCompactTreeLayout);
/**
* Variable: angleOffset
*
* The initial offset to compute the angle position.
*/
mxRadialTreeLayout.prototype.angleOffset = 0.5;
/**
* Variable: rootx
*
* The X co-ordinate of the root cell
*/
mxRadialTreeLayout.prototype.rootx = 0;
/**
* Variable: rooty
*
* The Y co-ordinate of the root cell
*/
mxRadialTreeLayout.prototype.rooty = 0;
/**
* Variable: levelDistance
*
* Holds the levelDistance. Default is 120.
*/
mxRadialTreeLayout.prototype.levelDistance = 120;
/**
* Variable: nodeDistance
*
* Holds the nodeDistance. Default is 10.
*/
mxRadialTreeLayout.prototype.nodeDistance = 10;
/**
* Variable: autoRadius
*
* Specifies if the radios should be computed automatically
*/
mxRadialTreeLayout.prototype.autoRadius = false;
/**
* Variable: sortEdges
*
* Specifies if edges should be sorted according to the order of their
* opposite terminal cell in the model.
*/
mxRadialTreeLayout.prototype.sortEdges = false;
/**
* Variable: rowMinX
*
* Array of leftmost x coordinate of each row
*/
mxRadialTreeLayout.prototype.rowMinX = [];
/**
* Variable: rowMaxX
*
* Array of rightmost x coordinate of each row
*/
mxRadialTreeLayout.prototype.rowMaxX = [];
/**
* Variable: rowMinCenX
*
* Array of x coordinate of leftmost vertex of each row
*/
mxRadialTreeLayout.prototype.rowMinCenX = [];
/**
* Variable: rowMaxCenX
*
* Array of x coordinate of rightmost vertex of each row
*/
mxRadialTreeLayout.prototype.rowMaxCenX = [];
/**
* Variable: rowRadi
*
* Array of y deltas of each row behind root vertex, also the radius in the tree
*/
mxRadialTreeLayout.prototype.rowRadi = [];
/**
* Variable: row
*
* Array of vertices on each row
*/
mxRadialTreeLayout.prototype.row = [];
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxRadialTreeLayout.prototype.isVertexIgnored = function(vertex)
{
return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
this.graph.getConnections(vertex).length == 0;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*
* If the parent has any connected edges, then it is used as the root of
* the tree. Else, <mxGraph.findTreeRoots> will be used to find a suitable
* root node within the set of children of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be laid out.
* root - Optional <mxCell> that will be used as the root of the tree.
*/
mxRadialTreeLayout.prototype.execute = function(parent, root)
{
this.parent = parent;
this.useBoundingBox = false;
this.edgeRouting = false;
//this.horizontal = false;
mxCompactTreeLayout.prototype.execute.apply(this, arguments);
var bounds = null;
var rootBounds = this.getVertexBounds(this.root);
this.centerX = rootBounds.x + rootBounds.width / 2;
this.centerY = rootBounds.y + rootBounds.height / 2;
// Calculate the bounds of the involved vertices directly from the values set in the compact tree
for (var vertex in this.visited)
{
var vertexBounds = this.getVertexBounds(this.visited[vertex]);
bounds = (bounds != null) ? bounds : vertexBounds.clone();
bounds.add(vertexBounds);
}
this.calcRowDims([this.node], 0);
var maxLeftGrad = 0;
var maxRightGrad = 0;
// Find the steepest left and right gradients
for (var i = 0; i < this.row.length; i++)
{
var leftGrad = (this.centerX - this.rowMinX[i] - this.nodeDistance) / this.rowRadi[i];
var rightGrad = (this.rowMaxX[i] - this.centerX - this.nodeDistance) / this.rowRadi[i];
maxLeftGrad = Math.max (maxLeftGrad, leftGrad);
maxRightGrad = Math.max (maxRightGrad, rightGrad);
}
// Extend out row so they meet the maximum gradient and convert to polar co-ords
for (var i = 0; i < this.row.length; i++)
{
var xLeftLimit = this.centerX - this.nodeDistance - maxLeftGrad * this.rowRadi[i];
var xRightLimit = this.centerX + this.nodeDistance + maxRightGrad * this.rowRadi[i];
var fullWidth = xRightLimit - xLeftLimit;
for (var j = 0; j < this.row[i].length; j ++)
{
var row = this.row[i];
var node = row[j];
var vertexBounds = this.getVertexBounds(node.cell);
var xProportion = (vertexBounds.x + vertexBounds.width / 2 - xLeftLimit) / (fullWidth);
var theta = 2 * Math.PI * xProportion;
node.theta = theta;
}
}
// Post-process from outside inwards to try to align parents with children
for (var i = this.row.length - 2; i >= 0; i--)
{
var row = this.row[i];
for (var j = 0; j < row.length; j++)
{
var node = row[j];
var child = node.child;
var counter = 0;
var totalTheta = 0;
while (child != null)
{
totalTheta += child.theta;
counter++;
child = child.next;
}
if (counter > 0)
{
var averTheta = totalTheta / counter;
if (averTheta > node.theta && j < row.length - 1)
{
var nextTheta = row[j+1].theta;
node.theta = Math.min (averTheta, nextTheta - Math.PI/10);
}
else if (averTheta < node.theta && j > 0 )
{
var lastTheta = row[j-1].theta;
node.theta = Math.max (averTheta, lastTheta + Math.PI/10);
}
}
}
}
// Set locations
for (var i = 0; i < this.row.length; i++)
{
for (var j = 0; j < this.row[i].length; j ++)
{
var row = this.row[i];
var node = row[j];
var vertexBounds = this.getVertexBounds(node.cell);
this.setVertexLocation(node.cell,
this.centerX - vertexBounds.width / 2 + this.rowRadi[i] * Math.cos(node.theta),
this.centerY - vertexBounds.height / 2 + this.rowRadi[i] * Math.sin(node.theta));
}
}
};
/**
* Function: calcRowDims
*
* Recursive function to calculate the dimensions of each row
*
* Parameters:
*
* row - Array of internal nodes, the children of which are to be processed.
* rowNum - Integer indicating which row is being processed.
*/
mxRadialTreeLayout.prototype.calcRowDims = function(row, rowNum)
{
if (row == null || row.length == 0)
{
return;
}
// Place root's children proportionally around the first level
this.rowMinX[rowNum] = this.centerX;
this.rowMaxX[rowNum] = this.centerX;
this.rowMinCenX[rowNum] = this.centerX;
this.rowMaxCenX[rowNum] = this.centerX;
this.row[rowNum] = [];
var rowHasChildren = false;
for (var i = 0; i < row.length; i++)
{
var child = row[i] != null ? row[i].child : null;
while (child != null)
{
var cell = child.cell;
var vertexBounds = this.getVertexBounds(cell);
this.rowMinX[rowNum] = Math.min(vertexBounds.x, this.rowMinX[rowNum]);
this.rowMaxX[rowNum] = Math.max(vertexBounds.x + vertexBounds.width, this.rowMaxX[rowNum]);
this.rowMinCenX[rowNum] = Math.min(vertexBounds.x + vertexBounds.width / 2, this.rowMinCenX[rowNum]);
this.rowMaxCenX[rowNum] = Math.max(vertexBounds.x + vertexBounds.width / 2, this.rowMaxCenX[rowNum]);
this.rowRadi[rowNum] = vertexBounds.y - this.getVertexBounds(this.root).y;
if (child.child != null)
{
rowHasChildren = true;
}
this.row[rowNum].push(child);
child = child.next;
}
}
if (rowHasChildren)
{
this.calcRowDims(this.row[rowNum], rowNum + 1);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxFastOrganicLayout
*
* Extends <mxGraphLayout> to implement a fast organic layout algorithm.
* The vertices need to be connected for this layout to work, vertices
* with no connections are ignored.
*
* Example:
*
* (code)
* var layout = new mxFastOrganicLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCompactTreeLayout
*
* Constructs a new fast organic layout for the specified graph.
*/
function mxFastOrganicLayout(graph)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxFastOrganicLayout.prototype = new mxGraphLayout();
mxFastOrganicLayout.prototype.constructor = mxFastOrganicLayout;
/**
* Variable: useInputOrigin
*
* Specifies if the top left corner of the input cells should be the origin
* of the layout result. Default is true.
*/
mxFastOrganicLayout.prototype.useInputOrigin = true;
/**
* Variable: resetEdges
*
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
mxFastOrganicLayout.prototype.resetEdges = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxFastOrganicLayout.prototype.disableEdgeStyle = true;
/**
* Variable: forceConstant
*
* The force constant by which the attractive forces are divided and the
* replusive forces are multiple by the square of. The value equates to the
* average radius there is of free space around each node. Default is 50.
*/
mxFastOrganicLayout.prototype.forceConstant = 50;
/**
* Variable: forceConstantSquared
*
* Cache of <forceConstant>^2 for performance.
*/
mxFastOrganicLayout.prototype.forceConstantSquared = 0;
/**
* Variable: minDistanceLimit
*
* Minimal distance limit. Default is 2. Prevents of
* dividing by zero.
*/
mxFastOrganicLayout.prototype.minDistanceLimit = 2;
/**
* Variable: minDistanceLimit
*
* Minimal distance limit. Default is 2. Prevents of
* dividing by zero.
*/
mxFastOrganicLayout.prototype.maxDistanceLimit = 500;
/**
* Variable: minDistanceLimitSquared
*
* Cached version of <minDistanceLimit> squared.
*/
mxFastOrganicLayout.prototype.minDistanceLimitSquared = 4;
/**
* Variable: initialTemp
*
* Start value of temperature. Default is 200.
*/
mxFastOrganicLayout.prototype.initialTemp = 200;
/**
* Variable: temperature
*
* Temperature to limit displacement at later stages of layout.
*/
mxFastOrganicLayout.prototype.temperature = 0;
/**
* Variable: maxIterations
*
* Total number of iterations to run the layout though.
*/
mxFastOrganicLayout.prototype.maxIterations = 0;
/**
* Variable: iteration
*
* Current iteration count.
*/
mxFastOrganicLayout.prototype.iteration = 0;
/**
* Variable: vertexArray
*
* An array of all vertices to be laid out.
*/
mxFastOrganicLayout.prototype.vertexArray;
/**
* Variable: dispX
*
* An array of locally stored X co-ordinate displacements for the vertices.
*/
mxFastOrganicLayout.prototype.dispX;
/**
* Variable: dispY
*
* An array of locally stored Y co-ordinate displacements for the vertices.
*/
mxFastOrganicLayout.prototype.dispY;
/**
* Variable: cellLocation
*
* An array of locally stored co-ordinate positions for the vertices.
*/
mxFastOrganicLayout.prototype.cellLocation;
/**
* Variable: radius
*
* The approximate radius of each cell, nodes only.
*/
mxFastOrganicLayout.prototype.radius;
/**
* Variable: radiusSquared
*
* The approximate radius squared of each cell, nodes only.
*/
mxFastOrganicLayout.prototype.radiusSquared;
/**
* Variable: isMoveable
*
* Array of booleans representing the movable states of the vertices.
*/
mxFastOrganicLayout.prototype.isMoveable;
/**
* Variable: neighbours
*
* Local copy of cell neighbours.
*/
mxFastOrganicLayout.prototype.neighbours;
/**
* Variable: indices
*
* Hashtable from cells to local indices.
*/
mxFastOrganicLayout.prototype.indices;
/**
* Variable: allowedToRun
*
* Boolean flag that specifies if the layout is allowed to run. If this is
* set to false, then the layout exits in the following iteration.
*/
mxFastOrganicLayout.prototype.allowedToRun = true;
/**
* Function: isVertexIgnored
*
* Returns a boolean indicating if the given <mxCell> should be ignored as a
* vertex. This returns true if the cell has no connections.
*
* Parameters:
*
* vertex - <mxCell> whose ignored state should be returned.
*/
mxFastOrganicLayout.prototype.isVertexIgnored = function(vertex)
{
return mxGraphLayout.prototype.isVertexIgnored.apply(this, arguments) ||
this.graph.getConnections(vertex).length == 0;
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>. This operates on all children of the
* given parent where <isVertexIgnored> returns false.
*/
mxFastOrganicLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
this.vertexArray = [];
var cells = this.graph.getChildVertices(parent);
for (var i = 0; i < cells.length; i++)
{
if (!this.isVertexIgnored(cells[i]))
{
this.vertexArray.push(cells[i]);
}
}
var initialBounds = (this.useInputOrigin) ?
this.graph.getBoundingBoxFromGeometry(this.vertexArray) :
null;
var n = this.vertexArray.length;
this.indices = [];
this.dispX = [];
this.dispY = [];
this.cellLocation = [];
this.isMoveable = [];
this.neighbours = [];
this.radius = [];
this.radiusSquared = [];
if (this.forceConstant < 0.001)
{
this.forceConstant = 0.001;
}
this.forceConstantSquared = this.forceConstant * this.forceConstant;
// Create a map of vertices first. This is required for the array of
// arrays called neighbours which holds, for each vertex, a list of
// ints which represents the neighbours cells to that vertex as
// the indices into vertexArray
for (var i = 0; i < this.vertexArray.length; i++)
{
var vertex = this.vertexArray[i];
this.cellLocation[i] = [];
// Set up the mapping from array indices to cells
var id = mxObjectIdentity.get(vertex);
this.indices[id] = i;
var bounds = this.getVertexBounds(vertex);
// Set the X,Y value of the internal version of the cell to
// the center point of the vertex for better positioning
var width = bounds.width;
var height = bounds.height;
// Randomize (0, 0) locations
var x = bounds.x;
var y = bounds.y;
this.cellLocation[i][0] = x + width / 2.0;
this.cellLocation[i][1] = y + height / 2.0;
this.radius[i] = Math.min(width, height);
this.radiusSquared[i] = this.radius[i] * this.radius[i];
}
// Moves cell location back to top-left from center locations used in
// algorithm, resetting the edge points is part of the transaction
model.beginUpdate();
try
{
for (var i = 0; i < n; i++)
{
this.dispX[i] = 0;
this.dispY[i] = 0;
this.isMoveable[i] = this.isVertexMovable(this.vertexArray[i]);
// Get lists of neighbours to all vertices, translate the cells
// obtained in indices into vertexArray and store as an array
// against the orginial cell index
var edges = this.graph.getConnections(this.vertexArray[i], parent);
var cells = this.graph.getOpposites(edges, this.vertexArray[i]);
this.neighbours[i] = [];
for (var j = 0; j < cells.length; j++)
{
// Resets the points on the traversed edge
if (this.resetEdges)
{
this.graph.resetEdge(edges[j]);
}
if (this.disableEdgeStyle)
{
this.setEdgeStyleEnabled(edges[j], false);
}
// Looks the cell up in the indices dictionary
var id = mxObjectIdentity.get(cells[j]);
var index = this.indices[id];
// Check the connected cell in part of the vertex list to be
// acted on by this layout
if (index != null)
{
this.neighbours[i][j] = index;
}
// Else if index of the other cell doesn't correspond to
// any cell listed to be acted upon in this layout. Set
// the index to the value of this vertex (a dummy self-loop)
// so the attraction force of the edge is not calculated
else
{
this.neighbours[i][j] = i;
}
}
}
this.temperature = this.initialTemp;
// If max number of iterations has not been set, guess it
if (this.maxIterations == 0)
{
this.maxIterations = 20 * Math.sqrt(n);
}
// Main iteration loop
for (this.iteration = 0; this.iteration < this.maxIterations; this.iteration++)
{
if (!this.allowedToRun)
{
return;
}
// Calculate repulsive forces on all vertices
this.calcRepulsion();
// Calculate attractive forces through edges
this.calcAttraction();
this.calcPositions();
this.reduceTemperature();
}
var minx = null;
var miny = null;
for (var i = 0; i < this.vertexArray.length; i++)
{
var vertex = this.vertexArray[i];
if (this.isVertexMovable(vertex))
{
var bounds = this.getVertexBounds(vertex);
if (bounds != null)
{
this.cellLocation[i][0] -= bounds.width / 2.0;
this.cellLocation[i][1] -= bounds.height / 2.0;
var x = this.graph.snap(Math.round(this.cellLocation[i][0]));
var y = this.graph.snap(Math.round(this.cellLocation[i][1]));
this.setVertexLocation(vertex, x, y);
if (minx == null)
{
minx = x;
}
else
{
minx = Math.min(minx, x);
}
if (miny == null)
{
miny = y;
}
else
{
miny = Math.min(miny, y);
}
}
}
}
// Modifies the cloned geometries in-place. Not needed
// to clone the geometries again as we're in the same
// undoable change.
var dx = -(minx || 0) + 1;
var dy = -(miny || 0) + 1;
if (initialBounds != null)
{
dx += initialBounds.x;
dy += initialBounds.y;
}
this.graph.moveCells(this.vertexArray, dx, dy);
}
finally
{
model.endUpdate();
}
};
/**
* Function: calcPositions
*
* Takes the displacements calculated for each cell and applies them to the
* local cache of cell positions. Limits the displacement to the current
* temperature.
*/
mxFastOrganicLayout.prototype.calcPositions = function()
{
for (var index = 0; index < this.vertexArray.length; index++)
{
if (this.isMoveable[index])
{
// Get the distance of displacement for this node for this
// iteration
var deltaLength = Math.sqrt(this.dispX[index] * this.dispX[index] +
this.dispY[index] * this.dispY[index]);
if (deltaLength < 0.001)
{
deltaLength = 0.001;
}
// Scale down by the current temperature if less than the
// displacement distance
var newXDisp = this.dispX[index] / deltaLength
* Math.min(deltaLength, this.temperature);
var newYDisp = this.dispY[index] / deltaLength
* Math.min(deltaLength, this.temperature);
// reset displacements
this.dispX[index] = 0;
this.dispY[index] = 0;
// Update the cached cell locations
this.cellLocation[index][0] += newXDisp;
this.cellLocation[index][1] += newYDisp;
}
}
};
/**
* Function: calcAttraction
*
* Calculates the attractive forces between all laid out nodes linked by
* edges
*/
mxFastOrganicLayout.prototype.calcAttraction = function()
{
// Check the neighbours of each vertex and calculate the attractive
// force of the edge connecting them
for (var i = 0; i < this.vertexArray.length; i++)
{
for (var k = 0; k < this.neighbours[i].length; k++)
{
// Get the index of the othe cell in the vertex array
var j = this.neighbours[i][k];
// Do not proceed self-loops
if (i != j &&
this.isMoveable[i] &&
this.isMoveable[j])
{
var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
// The distance between the nodes
var deltaLengthSquared = xDelta * xDelta + yDelta
* yDelta - this.radiusSquared[i] - this.radiusSquared[j];
if (deltaLengthSquared < this.minDistanceLimitSquared)
{
deltaLengthSquared = this.minDistanceLimitSquared;
}
var deltaLength = Math.sqrt(deltaLengthSquared);
var force = (deltaLengthSquared) / this.forceConstant;
var displacementX = (xDelta / deltaLength) * force;
var displacementY = (yDelta / deltaLength) * force;
this.dispX[i] -= displacementX;
this.dispY[i] -= displacementY;
this.dispX[j] += displacementX;
this.dispY[j] += displacementY;
}
}
}
};
/**
* Function: calcRepulsion
*
* Calculates the repulsive forces between all laid out nodes
*/
mxFastOrganicLayout.prototype.calcRepulsion = function()
{
var vertexCount = this.vertexArray.length;
for (var i = 0; i < vertexCount; i++)
{
for (var j = i; j < vertexCount; j++)
{
// Exits if the layout is no longer allowed to run
if (!this.allowedToRun)
{
return;
}
if (j != i &&
this.isMoveable[i] &&
this.isMoveable[j])
{
var xDelta = this.cellLocation[i][0] - this.cellLocation[j][0];
var yDelta = this.cellLocation[i][1] - this.cellLocation[j][1];
if (xDelta == 0)
{
xDelta = 0.01 + Math.random();
}
if (yDelta == 0)
{
yDelta = 0.01 + Math.random();
}
// Distance between nodes
var deltaLength = Math.sqrt((xDelta * xDelta)
+ (yDelta * yDelta));
var deltaLengthWithRadius = deltaLength - this.radius[i]
- this.radius[j];
if (deltaLengthWithRadius > this.maxDistanceLimit)
{
// Ignore vertices too far apart
continue;
}
if (deltaLengthWithRadius < this.minDistanceLimit)
{
deltaLengthWithRadius = this.minDistanceLimit;
}
var force = this.forceConstantSquared / deltaLengthWithRadius;
var displacementX = (xDelta / deltaLength) * force;
var displacementY = (yDelta / deltaLength) * force;
this.dispX[i] += displacementX;
this.dispY[i] += displacementY;
this.dispX[j] -= displacementX;
this.dispY[j] -= displacementY;
}
}
}
};
/**
* Function: reduceTemperature
*
* Reduces the temperature of the layout from an initial setting in a linear
* fashion to zero.
*/
mxFastOrganicLayout.prototype.reduceTemperature = function()
{
this.temperature = this.initialTemp * (1.0 - this.iteration / this.maxIterations);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCircleLayout
*
* Extends <mxGraphLayout> to implement a circluar layout for a given radius.
* The vertices do not need to be connected for this layout to work and all
* connections between vertices are not taken into account.
*
* Example:
*
* (code)
* var layout = new mxCircleLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCircleLayout
*
* Constructs a new circular layout for the specified radius.
*
* Arguments:
*
* graph - <mxGraph> that contains the cells.
* radius - Optional radius as an int. Default is 100.
*/
function mxCircleLayout(graph, radius)
{
mxGraphLayout.call(this, graph);
this.radius = (radius != null) ? radius : 100;
};
/**
* Extends mxGraphLayout.
*/
mxCircleLayout.prototype = new mxGraphLayout();
mxCircleLayout.prototype.constructor = mxCircleLayout;
/**
* Variable: radius
*
* Integer specifying the size of the radius. Default is 100.
*/
mxCircleLayout.prototype.radius = null;
/**
* Variable: moveCircle
*
* Boolean specifying if the circle should be moved to the top,
* left corner specified by <x0> and <y0>. Default is false.
*/
mxCircleLayout.prototype.moveCircle = false;
/**
* Variable: x0
*
* Integer specifying the left coordinate of the circle.
* Default is 0.
*/
mxCircleLayout.prototype.x0 = 0;
/**
* Variable: y0
*
* Integer specifying the top coordinate of the circle.
* Default is 0.
*/
mxCircleLayout.prototype.y0 = 0;
/**
* Variable: resetEdges
*
* Specifies if all edge points of traversed edges should be removed.
* Default is true.
*/
mxCircleLayout.prototype.resetEdges = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxCircleLayout.prototype.disableEdgeStyle = true;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxCircleLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
// Moves the vertices to build a circle. Makes sure the
// radius is large enough for the vertices to not
// overlap
model.beginUpdate();
try
{
// Gets all vertices inside the parent and finds
// the maximum dimension of the largest vertex
var max = 0;
var top = null;
var left = null;
var vertices = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
if (!this.isVertexIgnored(cell))
{
vertices.push(cell);
var bounds = this.getVertexBounds(cell);
if (top == null)
{
top = bounds.y;
}
else
{
top = Math.min(top, bounds.y);
}
if (left == null)
{
left = bounds.x;
}
else
{
left = Math.min(left, bounds.x);
}
max = Math.max(max, Math.max(bounds.width, bounds.height));
}
else if (!this.isEdgeIgnored(cell))
{
// Resets the points on the traversed edge
if (this.resetEdges)
{
this.graph.resetEdge(cell);
}
if (this.disableEdgeStyle)
{
this.setEdgeStyleEnabled(cell, false);
}
}
}
var r = this.getRadius(vertices.length, max);
// Moves the circle to the specified origin
if (this.moveCircle)
{
left = this.x0;
top = this.y0;
}
this.circle(vertices, r, left, top);
}
finally
{
model.endUpdate();
}
};
/**
* Function: getRadius
*
* Returns the radius to be used for the given vertex count. Max is the maximum
* width or height of all vertices in the layout.
*/
mxCircleLayout.prototype.getRadius = function(count, max)
{
return Math.max(count * max / Math.PI, this.radius);
};
/**
* Function: circle
*
* Executes the circular layout for the specified array
* of vertices and the given radius. This is called from
* <execute>.
*/
mxCircleLayout.prototype.circle = function(vertices, r, left, top)
{
var vertexCount = vertices.length;
var phi = 2 * Math.PI / vertexCount;
for (var i = 0; i < vertexCount; i++)
{
if (this.isVertexMovable(vertices[i]))
{
this.setVertexLocation(vertices[i],
Math.round(left + r + r * Math.sin(i * phi)),
Math.round(top + r + r * Math.cos(i * phi)));
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxParallelEdgeLayout
*
* Extends <mxGraphLayout> for arranging parallel edges. This layout works
* on edges for all pairs of vertices where there is more than one edge
* connecting the latter.
*
* Example:
*
* (code)
* var layout = new mxParallelEdgeLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* To run the layout for the parallel edges of a changed edge only, the
* following code can be used.
*
* (code)
* var layout = new mxParallelEdgeLayout(graph);
*
* graph.addListener(mxEvent.CELL_CONNECTED, function(sender, evt)
* {
* var model = graph.getModel();
* var edge = evt.getProperty('edge');
* var src = model.getTerminal(edge, true);
* var trg = model.getTerminal(edge, false);
*
* layout.isEdgeIgnored = function(edge2)
* {
* var src2 = model.getTerminal(edge2, true);
* var trg2 = model.getTerminal(edge2, false);
*
* return !(model.isEdge(edge2) && ((src == src2 && trg == trg2) || (src == trg2 && trg == src2)));
* };
*
* layout.execute(graph.getDefaultParent());
* });
* (end)
*
* Constructor: mxParallelEdgeLayout
*
* Constructs a new parallel edge layout for the specified graph.
*/
function mxParallelEdgeLayout(graph)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxParallelEdgeLayout.prototype = new mxGraphLayout();
mxParallelEdgeLayout.prototype.constructor = mxParallelEdgeLayout;
/**
* Variable: spacing
*
* Defines the spacing between the parallels. Default is 20.
*/
mxParallelEdgeLayout.prototype.spacing = 20;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxParallelEdgeLayout.prototype.execute = function(parent)
{
var lookup = this.findParallels(parent);
this.graph.model.beginUpdate();
try
{
for (var i in lookup)
{
var parallels = lookup[i];
if (parallels.length > 1)
{
this.layout(parallels);
}
}
}
finally
{
this.graph.model.endUpdate();
}
};
/**
* Function: findParallels
*
* Finds the parallel edges in the given parent.
*/
mxParallelEdgeLayout.prototype.findParallels = function(parent)
{
var model = this.graph.getModel();
var lookup = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (!this.isEdgeIgnored(child))
{
var id = this.getEdgeId(child);
if (id != null)
{
if (lookup[id] == null)
{
lookup[id] = [];
}
lookup[id].push(child);
}
}
}
return lookup;
};
/**
* Function: getEdgeId
*
* Returns a unique ID for the given edge. The id is independent of the
* edge direction and is built using the visible terminal of the given
* edge.
*/
mxParallelEdgeLayout.prototype.getEdgeId = function(edge)
{
var view = this.graph.getView();
// Cannot used cached visible terminal because this could be triggered in BEFORE_UNDO
var src = view.getVisibleTerminal(edge, true);
var trg = view.getVisibleTerminal(edge, false);
if (src != null && trg != null)
{
src = mxObjectIdentity.get(src);
trg = mxObjectIdentity.get(trg);
return (src > trg) ? trg + '-' + src : src + '-' + trg;
}
return null;
};
/**
* Function: layout
*
* Lays out the parallel edges in the given array.
*/
mxParallelEdgeLayout.prototype.layout = function(parallels)
{
var edge = parallels[0];
var view = this.graph.getView();
var model = this.graph.getModel();
var src = model.getGeometry(view.getVisibleTerminal(edge, true));
var trg = model.getGeometry(view.getVisibleTerminal(edge, false));
// Routes multiple loops
if (src == trg)
{
var x0 = src.x + src.width + this.spacing;
var y0 = src.y + src.height / 2;
for (var i = 0; i < parallels.length; i++)
{
this.route(parallels[i], x0, y0);
x0 += this.spacing;
}
}
else if (src != null && trg != null)
{
// Routes parallel edges
var scx = src.x + src.width / 2;
var scy = src.y + src.height / 2;
var tcx = trg.x + trg.width / 2;
var tcy = trg.y + trg.height / 2;
var dx = tcx - scx;
var dy = tcy - scy;
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 0)
{
var x0 = scx + dx / 2;
var y0 = scy + dy / 2;
var nx = dy * this.spacing / len;
var ny = dx * this.spacing / len;
x0 += nx * (parallels.length - 1) / 2;
y0 -= ny * (parallels.length - 1) / 2;
for (var i = 0; i < parallels.length; i++)
{
this.route(parallels[i], x0, y0);
x0 -= nx;
y0 += ny;
}
}
}
};
/**
* Function: route
*
* Routes the given edge via the given point.
*/
mxParallelEdgeLayout.prototype.route = function(edge, x, y)
{
if (this.graph.isCellMovable(edge))
{
this.setEdgePoints(edge, [new mxPoint(x, y)]);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCompositeLayout
*
* Allows to compose multiple layouts into a single layout. The master layout
* is the layout that handles move operations if another layout than the first
* element in <layouts> should be used. The <master> layout is not executed as
* the code assumes that it is part of <layouts>.
*
* Example:
* (code)
* var first = new mxFastOrganicLayout(graph);
* var second = new mxParallelEdgeLayout(graph);
* var layout = new mxCompositeLayout(graph, [first, second], first);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxCompositeLayout
*
* Constructs a new layout using the given layouts. The graph instance is
* required for creating the transaction that contains all layouts.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* layouts - Array of <mxGraphLayouts>.
* master - Optional layout that handles moves. If no layout is given then
* the first layout of the above array is used to handle moves.
*/
function mxCompositeLayout(graph, layouts, master)
{
mxGraphLayout.call(this, graph);
this.layouts = layouts;
this.master = master;
};
/**
* Extends mxGraphLayout.
*/
mxCompositeLayout.prototype = new mxGraphLayout();
mxCompositeLayout.prototype.constructor = mxCompositeLayout;
/**
* Variable: layouts
*
* Holds the array of <mxGraphLayouts> that this layout contains.
*/
mxCompositeLayout.prototype.layouts = null;
/**
* Variable: layouts
*
* Reference to the <mxGraphLayouts> that handles moves. If this is null
* then the first layout in <layouts> is used.
*/
mxCompositeLayout.prototype.master = null;
/**
* Function: moveCell
*
* Implements <mxGraphLayout.moveCell> by calling move on <master> or the first
* layout in <layouts>.
*/
mxCompositeLayout.prototype.moveCell = function(cell, x, y)
{
if (this.master != null)
{
this.master.moveCell.apply(this.master, arguments);
}
else
{
this.layouts[0].moveCell.apply(this.layouts[0], arguments);
}
};
/**
* Function: execute
*
* Implements <mxGraphLayout.execute> by executing all <layouts> in a
* single transaction.
*/
mxCompositeLayout.prototype.execute = function(parent)
{
var model = this.graph.getModel();
model.beginUpdate();
try
{
for (var i = 0; i < this.layouts.length; i++)
{
this.layouts[i].execute.apply(this.layouts[i], arguments);
}
}
finally
{
model.endUpdate();
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEdgeLabelLayout
*
* Extends <mxGraphLayout> to implement an edge label layout. This layout
* makes use of cell states, which means the graph must be validated in
* a graph view (so that the label bounds are available) before this layout
* can be executed.
*
* Example:
*
* (code)
* var layout = new mxEdgeLabelLayout(graph);
* layout.execute(graph.getDefaultParent());
* (end)
*
* Constructor: mxEdgeLabelLayout
*
* Constructs a new edge label layout.
*
* Arguments:
*
* graph - <mxGraph> that contains the cells.
*/
function mxEdgeLabelLayout(graph, radius)
{
mxGraphLayout.call(this, graph);
};
/**
* Extends mxGraphLayout.
*/
mxEdgeLabelLayout.prototype = new mxGraphLayout();
mxEdgeLabelLayout.prototype.constructor = mxEdgeLabelLayout;
/**
* Function: execute
*
* Implements <mxGraphLayout.execute>.
*/
mxEdgeLabelLayout.prototype.execute = function(parent)
{
var view = this.graph.view;
var model = this.graph.getModel();
// Gets all vertices and edges inside the parent
var edges = [];
var vertices = [];
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
var state = view.getState(cell);
if (state != null)
{
if (!this.isVertexIgnored(cell))
{
vertices.push(state);
}
else if (!this.isEdgeIgnored(cell))
{
edges.push(state);
}
}
}
this.placeLabels(vertices, edges);
};
/**
* Function: placeLabels
*
* Places the labels of the given edges.
*/
mxEdgeLabelLayout.prototype.placeLabels = function(v, e)
{
var model = this.graph.getModel();
// Moves the vertices to build a circle. Makes sure the
// radius is large enough for the vertices to not
// overlap
model.beginUpdate();
try
{
for (var i = 0; i < e.length; i++)
{
var edge = e[i];
if (edge != null && edge.text != null &&
edge.text.boundingBox != null)
{
for (var j = 0; j < v.length; j++)
{
var vertex = v[j];
if (vertex != null)
{
this.avoid(edge, vertex);
}
}
}
}
}
finally
{
model.endUpdate();
}
};
/**
* Function: avoid
*
* Places the labels of the given edges.
*/
mxEdgeLabelLayout.prototype.avoid = function(edge, vertex)
{
var model = this.graph.getModel();
var labRect = edge.text.boundingBox;
if (mxUtils.intersects(labRect, vertex))
{
var dy1 = -labRect.y - labRect.height + vertex.y;
var dy2 = -labRect.y + vertex.y + vertex.height;
var dy = (Math.abs(dy1) < Math.abs(dy2)) ? dy1 : dy2;
var dx1 = -labRect.x - labRect.width + vertex.x;
var dx2 = -labRect.x + vertex.x + vertex.width;
var dx = (Math.abs(dx1) < Math.abs(dx2)) ? dx1 : dx2;
if (Math.abs(dx) < Math.abs(dy))
{
dy = 0;
}
else
{
dx = 0;
}
var g = model.getGeometry(edge.cell);
if (g != null)
{
g = g.clone();
if (g.offset != null)
{
g.offset.x += dx;
g.offset.y += dy;
}
else
{
g.offset = new mxPoint(dx, dy);
}
model.setGeometry(edge.cell, g);
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphAbstractHierarchyCell
*
* An abstraction of an internal hierarchy node or edge
*
* Constructor: mxGraphAbstractHierarchyCell
*
* Constructs a new hierarchical layout algorithm.
*/
function mxGraphAbstractHierarchyCell()
{
this.x = [];
this.y = [];
this.temp = [];
};
/**
* Variable: maxRank
*
* The maximum rank this cell occupies. Default is -1.
*/
mxGraphAbstractHierarchyCell.prototype.maxRank = -1;
/**
* Variable: minRank
*
* The minimum rank this cell occupies. Default is -1.
*/
mxGraphAbstractHierarchyCell.prototype.minRank = -1;
/**
* Variable: x
*
* The x position of this cell for each layer it occupies
*/
mxGraphAbstractHierarchyCell.prototype.x = null;
/**
* Variable: y
*
* The y position of this cell for each layer it occupies
*/
mxGraphAbstractHierarchyCell.prototype.y = null;
/**
* Variable: width
*
* The width of this cell. Default is 0.
*/
mxGraphAbstractHierarchyCell.prototype.width = 0;
/**
* Variable: height
*
* The height of this cell. Default is 0.
*/
mxGraphAbstractHierarchyCell.prototype.height = 0;
/**
* Variable: nextLayerConnectedCells
*
* A cached version of the cells this cell connects to on the next layer up
*/
mxGraphAbstractHierarchyCell.prototype.nextLayerConnectedCells = null;
/**
* Variable: previousLayerConnectedCells
*
* A cached version of the cells this cell connects to on the next layer down
*/
mxGraphAbstractHierarchyCell.prototype.previousLayerConnectedCells = null;
/**
* Variable: temp
*
* Temporary variable for general use. Generally, try to avoid
* carrying information between stages. Currently, the longest
* path layering sets temp to the rank position in fixRanks()
* and the crossing reduction uses this. This meant temp couldn't
* be used for hashing the nodes in the model dfs and so hashCode
* was created
*/
mxGraphAbstractHierarchyCell.prototype.temp = null;
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphAbstractHierarchyCell.prototype.getNextLayerConnectedCells = function(layer)
{
return null;
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphAbstractHierarchyCell.prototype.getPreviousLayerConnectedCells = function(layer)
{
return null;
};
/**
* Function: isEdge
*
* Returns whether or not this cell is an edge
*/
mxGraphAbstractHierarchyCell.prototype.isEdge = function()
{
return false;
};
/**
* Function: isVertex
*
* Returns whether or not this cell is a node
*/
mxGraphAbstractHierarchyCell.prototype.isVertex = function()
{
return false;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.getGeneralPurposeVariable = function(layer)
{
return null;
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setGeneralPurposeVariable = function(layer, value)
{
return null;
};
/**
* Function: setX
*
* Set the value of x for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setX = function(layer, value)
{
if (this.isVertex())
{
this.x[0] = value;
}
else if (this.isEdge())
{
this.x[layer - this.minRank - 1] = value;
}
};
/**
* Function: getX
*
* Gets the value of x on the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.getX = function(layer)
{
if (this.isVertex())
{
return this.x[0];
}
else if (this.isEdge())
{
return this.x[layer - this.minRank - 1];
}
return 0.0;
};
/**
* Function: setY
*
* Set the value of y for the specified layer
*/
mxGraphAbstractHierarchyCell.prototype.setY = function(layer, value)
{
if (this.isVertex())
{
this.y[0] = value;
}
else if (this.isEdge())
{
this.y[layer -this. minRank - 1] = value;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyNode
*
* An abstraction of a hierarchical edge for the hierarchy layout
*
* Constructor: mxGraphHierarchyNode
*
* Constructs an internal node to represent the specified real graph cell
*
* Arguments:
*
* cell - the real graph cell this node represents
*/
function mxGraphHierarchyNode(cell)
{
mxGraphAbstractHierarchyCell.apply(this, arguments);
this.cell = cell;
this.id = mxObjectIdentity.get(cell);
this.connectsAsTarget = [];
this.connectsAsSource = [];
};
/**
* Extends mxGraphAbstractHierarchyCell.
*/
mxGraphHierarchyNode.prototype = new mxGraphAbstractHierarchyCell();
mxGraphHierarchyNode.prototype.constructor = mxGraphHierarchyNode;
/**
* Variable: cell
*
* The graph cell this object represents.
*/
mxGraphHierarchyNode.prototype.cell = null;
/**
* Variable: id
*
* The object identity of the wrapped cell
*/
mxGraphHierarchyNode.prototype.id = null;
/**
* Variable: connectsAsTarget
*
* Collection of hierarchy edges that have this node as a target
*/
mxGraphHierarchyNode.prototype.connectsAsTarget = null;
/**
* Variable: connectsAsSource
*
* Collection of hierarchy edges that have this node as a source
*/
mxGraphHierarchyNode.prototype.connectsAsSource = null;
/**
* Variable: hashCode
*
* Assigns a unique hashcode for each node. Used by the model dfs instead
* of copying HashSets
*/
mxGraphHierarchyNode.prototype.hashCode = false;
/**
* Function: getRankValue
*
* Returns the integer value of the layer that this node resides in
*/
mxGraphHierarchyNode.prototype.getRankValue = function(layer)
{
return this.maxRank;
};
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphHierarchyNode.prototype.getNextLayerConnectedCells = function(layer)
{
if (this.nextLayerConnectedCells == null)
{
this.nextLayerConnectedCells = [];
this.nextLayerConnectedCells[0] = [];
for (var i = 0; i < this.connectsAsTarget.length; i++)
{
var edge = this.connectsAsTarget[i];
if (edge.maxRank == -1 || edge.maxRank == layer + 1)
{
// Either edge is not in any rank or
// no dummy nodes in edge, add node of other side of edge
this.nextLayerConnectedCells[0].push(edge.source);
}
else
{
// Edge spans at least two layers, add edge
this.nextLayerConnectedCells[0].push(edge);
}
}
}
return this.nextLayerConnectedCells[0];
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphHierarchyNode.prototype.getPreviousLayerConnectedCells = function(layer)
{
if (this.previousLayerConnectedCells == null)
{
this.previousLayerConnectedCells = [];
this.previousLayerConnectedCells[0] = [];
for (var i = 0; i < this.connectsAsSource.length; i++)
{
var edge = this.connectsAsSource[i];
if (edge.minRank == -1 || edge.minRank == layer - 1)
{
// No dummy nodes in edge, add node of other side of edge
this.previousLayerConnectedCells[0].push(edge.target);
}
else
{
// Edge spans at least two layers, add edge
this.previousLayerConnectedCells[0].push(edge);
}
}
}
return this.previousLayerConnectedCells[0];
};
/**
* Function: isVertex
*
* Returns true.
*/
mxGraphHierarchyNode.prototype.isVertex = function()
{
return true;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphHierarchyNode.prototype.getGeneralPurposeVariable = function(layer)
{
return this.temp[0];
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphHierarchyNode.prototype.setGeneralPurposeVariable = function(layer, value)
{
this.temp[0] = value;
};
/**
* Function: isAncestor
*/
mxGraphHierarchyNode.prototype.isAncestor = function(otherNode)
{
// Firstly, the hash code of this node needs to be shorter than the
// other node
if (otherNode != null && this.hashCode != null && otherNode.hashCode != null
&& this.hashCode.length < otherNode.hashCode.length)
{
if (this.hashCode == otherNode.hashCode)
{
return true;
}
if (this.hashCode == null || this.hashCode == null)
{
return false;
}
// Secondly, this hash code must match the start of the other
// node's hash code. Arrays.equals cannot be used here since
// the arrays are different length, and we do not want to
// perform another array copy.
for (var i = 0; i < this.hashCode.length; i++)
{
if (this.hashCode[i] != otherNode.hashCode[i])
{
return false;
}
}
return true;
}
return false;
};
/**
* Function: getCoreCell
*
* Gets the core vertex associated with this wrapper
*/
mxGraphHierarchyNode.prototype.getCoreCell = function()
{
return this.cell;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyEdge
*
* An abstraction of a hierarchical edge for the hierarchy layout
*
* Constructor: mxGraphHierarchyEdge
*
* Constructs a hierarchy edge
*
* Arguments:
*
* edges - a list of real graph edges this abstraction represents
*/
function mxGraphHierarchyEdge(edges)
{
mxGraphAbstractHierarchyCell.apply(this, arguments);
this.edges = edges;
this.ids = [];
for (var i = 0; i < edges.length; i++)
{
this.ids.push(mxObjectIdentity.get(edges[i]));
}
};
/**
* Extends mxGraphAbstractHierarchyCell.
*/
mxGraphHierarchyEdge.prototype = new mxGraphAbstractHierarchyCell();
mxGraphHierarchyEdge.prototype.constructor = mxGraphHierarchyEdge;
/**
* Variable: edges
*
* The graph edge(s) this object represents. Parallel edges are all grouped
* together within one hierarchy edge.
*/
mxGraphHierarchyEdge.prototype.edges = null;
/**
* Variable: ids
*
* The object identities of the wrapped cells
*/
mxGraphHierarchyEdge.prototype.ids = null;
/**
* Variable: source
*
* The node this edge is sourced at
*/
mxGraphHierarchyEdge.prototype.source = null;
/**
* Variable: target
*
* The node this edge targets
*/
mxGraphHierarchyEdge.prototype.target = null;
/**
* Variable: isReversed
*
* Whether or not the direction of this edge has been reversed
* internally to create a DAG for the hierarchical layout
*/
mxGraphHierarchyEdge.prototype.isReversed = false;
/**
* Function: invert
*
* Inverts the direction of this internal edge(s)
*/
mxGraphHierarchyEdge.prototype.invert = function(layer)
{
var temp = this.source;
this.source = this.target;
this.target = temp;
this.isReversed = !this.isReversed;
};
/**
* Function: getNextLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer up
*/
mxGraphHierarchyEdge.prototype.getNextLayerConnectedCells = function(layer)
{
if (this.nextLayerConnectedCells == null)
{
this.nextLayerConnectedCells = [];
for (var i = 0; i < this.temp.length; i++)
{
this.nextLayerConnectedCells[i] = [];
if (i == this.temp.length - 1)
{
this.nextLayerConnectedCells[i].push(this.source);
}
else
{
this.nextLayerConnectedCells[i].push(this);
}
}
}
return this.nextLayerConnectedCells[layer - this.minRank - 1];
};
/**
* Function: getPreviousLayerConnectedCells
*
* Returns the cells this cell connects to on the next layer down
*/
mxGraphHierarchyEdge.prototype.getPreviousLayerConnectedCells = function(layer)
{
if (this.previousLayerConnectedCells == null)
{
this.previousLayerConnectedCells = [];
for (var i = 0; i < this.temp.length; i++)
{
this.previousLayerConnectedCells[i] = [];
if (i == 0)
{
this.previousLayerConnectedCells[i].push(this.target);
}
else
{
this.previousLayerConnectedCells[i].push(this);
}
}
}
return this.previousLayerConnectedCells[layer - this.minRank - 1];
};
/**
* Function: isEdge
*
* Returns true.
*/
mxGraphHierarchyEdge.prototype.isEdge = function()
{
return true;
};
/**
* Function: getGeneralPurposeVariable
*
* Gets the value of temp for the specified layer
*/
mxGraphHierarchyEdge.prototype.getGeneralPurposeVariable = function(layer)
{
return this.temp[layer - this.minRank - 1];
};
/**
* Function: setGeneralPurposeVariable
*
* Set the value of temp for the specified layer
*/
mxGraphHierarchyEdge.prototype.setGeneralPurposeVariable = function(layer, value)
{
this.temp[layer - this.minRank - 1] = value;
};
/**
* Function: getCoreCell
*
* Gets the first core edge associated with this wrapper
*/
mxGraphHierarchyEdge.prototype.getCoreCell = function()
{
if (this.edges != null && this.edges.length > 0)
{
return this.edges[0];
}
return null;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHierarchyModel
*
* Internal model of a hierarchical graph. This model stores nodes and edges
* equivalent to the real graph nodes and edges, but also stores the rank of the
* cells, the order within the ranks and the new candidate locations of cells.
* The internal model also reverses edge direction were appropriate , ignores
* self-loop and groups parallels together under one edge object.
*
* Constructor: mxGraphHierarchyModel
*
* Creates an internal ordered graph model using the vertices passed in. If
* there are any, leftward edge need to be inverted in the internal model
*
* Arguments:
*
* graph - the facade describing the graph to be operated on
* vertices - the vertices for this hierarchy
* ordered - whether or not the vertices are already ordered
* deterministic - whether or not this layout should be deterministic on each
* tightenToSource - whether or not to tighten vertices towards the sources
* scanRanksFromSinks - Whether rank assignment is from the sinks or sources.
* usage
*/
function mxGraphHierarchyModel(layout, vertices, roots, parent, tightenToSource)
{
var graph = layout.getGraph();
this.tightenToSource = tightenToSource;
this.roots = roots;
this.parent = parent;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly
this.vertexMapper = new mxDictionary();
this.edgeMapper = new mxDictionary();
this.maxRank = 0;
var internalVertices = [];
if (vertices == null)
{
vertices = this.graph.getChildVertices(parent);
}
this.maxRank = this.SOURCESCANSTARTRANK;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly. Guess size by number
// of edges is roughly same as number of vertices.
this.createInternalCells(layout, vertices, internalVertices);
// Go through edges set their sink values. Also check the
// ordering if and invert edges if necessary
for (var i = 0; i < vertices.length; i++)
{
var edges = internalVertices[i].connectsAsSource;
for (var j = 0; j < edges.length; j++)
{
var internalEdge = edges[j];
var realEdges = internalEdge.edges;
// Only need to process the first real edge, since
// all the edges connect to the same other vertex
if (realEdges != null && realEdges.length > 0)
{
var realEdge = realEdges[0];
var targetCell = layout.getVisibleTerminal(
realEdge, false);
var internalTargetCell = this.vertexMapper.get(targetCell);
if (internalVertices[i] == internalTargetCell)
{
// If there are parallel edges going between two vertices and not all are in the same direction
// you can have navigated across one direction when doing the cycle reversal that isn't the same
// direction as the first real edge in the array above. When that happens the if above catches
// that and we correct the target cell before continuing.
// This branch only detects this single case
targetCell = layout.getVisibleTerminal(
realEdge, true);
internalTargetCell = this.vertexMapper.get(targetCell);
}
if (internalTargetCell != null
&& internalVertices[i] != internalTargetCell)
{
internalEdge.target = internalTargetCell;
if (internalTargetCell.connectsAsTarget.length == 0)
{
internalTargetCell.connectsAsTarget = [];
}
if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0)
{
internalTargetCell.connectsAsTarget.push(internalEdge);
}
}
}
}
// Use the temp variable in the internal nodes to mark this
// internal vertex as having been visited.
internalVertices[i].temp[0] = 1;
}
};
/**
* Variable: maxRank
*
* Stores the largest rank number allocated
*/
mxGraphHierarchyModel.prototype.maxRank = null;
/**
* Variable: vertexMapper
*
* Map from graph vertices to internal model nodes.
*/
mxGraphHierarchyModel.prototype.vertexMapper = null;
/**
* Variable: edgeMapper
*
* Map from graph edges to internal model edges
*/
mxGraphHierarchyModel.prototype.edgeMapper = null;
/**
* Variable: ranks
*
* Mapping from rank number to actual rank
*/
mxGraphHierarchyModel.prototype.ranks = null;
/**
* Variable: roots
*
* Store of roots of this hierarchy model, these are real graph cells, not
* internal cells
*/
mxGraphHierarchyModel.prototype.roots = null;
/**
* Variable: parent
*
* The parent cell whose children are being laid out
*/
mxGraphHierarchyModel.prototype.parent = null;
/**
* Variable: dfsCount
*
* Count of the number of times the ancestor dfs has been used.
*/
mxGraphHierarchyModel.prototype.dfsCount = 0;
/**
* Variable: SOURCESCANSTARTRANK
*
* High value to start source layering scan rank value from.
*/
mxGraphHierarchyModel.prototype.SOURCESCANSTARTRANK = 100000000;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells.
*/
mxGraphHierarchyModel.prototype.tightenToSource = false;
/**
* Function: createInternalCells
*
* Creates all edges in the internal model
*
* Parameters:
*
* layout - Reference to the <mxHierarchicalLayout> algorithm.
* vertices - Array of <mxCells> that represent the vertices whom are to
* have an internal representation created.
* internalVertices - The array of <mxGraphHierarchyNodes> to have their
* information filled in using the real vertices.
*/
mxGraphHierarchyModel.prototype.createInternalCells = function(layout, vertices, internalVertices)
{
var graph = layout.getGraph();
// Create internal edges
for (var i = 0; i < vertices.length; i++)
{
internalVertices[i] = new mxGraphHierarchyNode(vertices[i]);
this.vertexMapper.put(vertices[i], internalVertices[i]);
// If the layout is deterministic, order the cells
//List outgoingCells = graph.getNeighbours(vertices[i], deterministic);
var conns = layout.getEdges(vertices[i]);
internalVertices[i].connectsAsSource = [];
// Create internal edges, but don't do any rank assignment yet
// First use the information from the greedy cycle remover to
// invert the leftward edges internally
for (var j = 0; j < conns.length; j++)
{
var cell = layout.getVisibleTerminal(conns[j], false);
// Looking for outgoing edges only
if (cell != vertices[i] && layout.graph.model.isVertex(cell) &&
!layout.isVertexIgnored(cell))
{
// We process all edge between this source and its targets
// If there are edges going both ways, we need to collect
// them all into one internal edges to avoid looping problems
// later. We assume this direction (source -> target) is the
// natural direction if at least half the edges are going in
// that direction.
// The check below for edges[0] being in the vertex mapper is
// in case we've processed this the other way around
// (target -> source) and the number of edges in each direction
// are the same. All the graph edges will have been assigned to
// an internal edge going the other way, so we don't want to
// process them again
var undirectedEdges = layout.getEdgesBetween(vertices[i],
cell, false);
var directedEdges = layout.getEdgesBetween(vertices[i],
cell, true);
if (undirectedEdges != null &&
undirectedEdges.length > 0 &&
this.edgeMapper.get(undirectedEdges[0]) == null &&
directedEdges.length * 2 >= undirectedEdges.length)
{
var internalEdge = new mxGraphHierarchyEdge(undirectedEdges);
for (var k = 0; k < undirectedEdges.length; k++)
{
var edge = undirectedEdges[k];
this.edgeMapper.put(edge, internalEdge);
// Resets all point on the edge and disables the edge style
// without deleting it from the cell style
graph.resetEdge(edge);
if (layout.disableEdgeStyle)
{
layout.setEdgeStyleEnabled(edge, false);
layout.setOrthogonalEdge(edge,true);
}
}
internalEdge.source = internalVertices[i];
if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0)
{
internalVertices[i].connectsAsSource.push(internalEdge);
}
}
}
}
// Ensure temp variable is cleared from any previous use
internalVertices[i].temp[0] = 0;
}
};
/**
* Function: initialRank
*
* Basic determination of minimum layer ranking by working from from sources
* or sinks and working through each node in the relevant edge direction.
* Starting at the sinks is basically a longest path layering algorithm.
*/
mxGraphHierarchyModel.prototype.initialRank = function()
{
var startNodes = [];
if (this.roots != null)
{
for (var i = 0; i < this.roots.length; i++)
{
var internalNode = this.vertexMapper.get(this.roots[i]);
if (internalNode != null)
{
startNodes.push(internalNode);
}
}
}
var internalNodes = this.vertexMapper.getValues();
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] = -1;
}
var startNodesCopy = startNodes.slice();
while (startNodes.length > 0)
{
var internalNode = startNodes[0];
var layerDeterminingEdges;
var edgesToBeMarked;
layerDeterminingEdges = internalNode.connectsAsTarget;
edgesToBeMarked = internalNode.connectsAsSource;
// flag to keep track of whether or not all layer determining
// edges have been scanned
var allEdgesScanned = true;
// Work out the layer of this node from the layer determining
// edges. The minimum layer number of any node connected by one of
// the layer determining edges variable
var minimumLayer = this.SOURCESCANSTARTRANK;
for (var i = 0; i < layerDeterminingEdges.length; i++)
{
var internalEdge = layerDeterminingEdges[i];
if (internalEdge.temp[0] == 5270620)
{
// This edge has been scanned, get the layer of the
// node on the other end
var otherNode = internalEdge.source;
minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1);
}
else
{
allEdgesScanned = false;
break;
}
}
// If all edge have been scanned, assign the layer, mark all
// edges in the other direction and remove from the nodes list
if (allEdgesScanned)
{
internalNode.temp[0] = minimumLayer;
this.maxRank = Math.min(this.maxRank, minimumLayer);
if (edgesToBeMarked != null)
{
for (var i = 0; i < edgesToBeMarked.length; i++)
{
var internalEdge = edgesToBeMarked[i];
// Assign unique stamp ( y/m/d/h )
internalEdge.temp[0] = 5270620;
// Add node on other end of edge to LinkedList of
// nodes to be analysed
var otherNode = internalEdge.target;
// Only add node if it hasn't been assigned a layer
if (otherNode.temp[0] == -1)
{
startNodes.push(otherNode);
// Mark this other node as neither being
// unassigned nor assigned so it isn't
// added to this list again, but it's
// layer isn't used in any calculation.
otherNode.temp[0] = -2;
}
}
}
startNodes.shift();
}
else
{
// Not all the edges have been scanned, get to the back of
// the class and put the dunces cap on
var removedCell = startNodes.shift();
startNodes.push(internalNode);
if (removedCell == internalNode && startNodes.length == 1)
{
// This is an error condition, we can't get out of
// this loop. It could happen for more than one node
// but that's a lot harder to detect. Log the error
// TODO make log comment
break;
}
}
}
// Normalize the ranks down from their large starting value to place
// at least 1 sink on layer 0
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] -= this.maxRank;
}
// Tighten the rank 0 nodes as far as possible
for ( var i = 0; i < startNodesCopy.length; i++)
{
var internalNode = startNodesCopy[i];
var currentMaxLayer = 0;
var layerDeterminingEdges = internalNode.connectsAsSource;
for ( var j = 0; j < layerDeterminingEdges.length; j++)
{
var internalEdge = layerDeterminingEdges[j];
var otherNode = internalEdge.target;
internalNode.temp[0] = Math.max(currentMaxLayer,
otherNode.temp[0] + 1);
currentMaxLayer = internalNode.temp[0];
}
}
// Reset the maxRank to that which would be expected for a from-sink
// scan
this.maxRank = this.SOURCESCANSTARTRANK - this.maxRank;
};
/**
* Function: fixRanks
*
* Fixes the layer assignments to the values stored in the nodes. Also needs
* to create dummy nodes for edges that cross layers.
*/
mxGraphHierarchyModel.prototype.fixRanks = function()
{
var rankList = [];
this.ranks = [];
for (var i = 0; i < this.maxRank + 1; i++)
{
rankList[i] = [];
this.ranks[i] = rankList[i];
}
// Perform a DFS to obtain an initial ordering for each rank.
// Without doing this you would end up having to process
// crossings for a standard tree.
var rootsArray = null;
if (this.roots != null)
{
var oldRootsArray = this.roots;
rootsArray = [];
for (var i = 0; i < oldRootsArray.length; i++)
{
var cell = oldRootsArray[i];
var internalNode = this.vertexMapper.get(cell);
rootsArray[i] = internalNode;
}
}
this.visit(function(parent, node, edge, layer, seen)
{
if (seen == 0 && node.maxRank < 0 && node.minRank < 0)
{
rankList[node.temp[0]].push(node);
node.maxRank = node.temp[0];
node.minRank = node.temp[0];
// Set temp[0] to the nodes position in the rank
node.temp[0] = rankList[node.maxRank].length - 1;
}
if (parent != null && edge != null)
{
var parentToCellRankDifference = parent.maxRank - node.maxRank;
if (parentToCellRankDifference > 1)
{
// There are ranks in between the parent and current cell
edge.maxRank = parent.maxRank;
edge.minRank = node.maxRank;
edge.temp = [];
edge.x = [];
edge.y = [];
for (var i = edge.minRank + 1; i < edge.maxRank; i++)
{
// The connecting edge must be added to the
// appropriate ranks
rankList[i].push(edge);
edge.setGeneralPurposeVariable(i, rankList[i]
.length - 1);
}
}
}
}, rootsArray, false, null);
};
/**
* Function: visit
*
* A depth first search through the internal heirarchy model.
*
* Parameters:
*
* visitor - The visitor function pattern to be called for each node.
* trackAncestors - Whether or not the search is to keep track all nodes
* directly above this one in the search path.
*/
mxGraphHierarchyModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes)
{
// Run dfs through on all roots
if (dfsRoots != null)
{
for (var i = 0; i < dfsRoots.length; i++)
{
var internalNode = dfsRoots[i];
if (internalNode != null)
{
if (seenNodes == null)
{
seenNodes = new Object();
}
if (trackAncestors)
{
// Set up hash code for root
internalNode.hashCode = [];
internalNode.hashCode[0] = this.dfsCount;
internalNode.hashCode[1] = i;
this.extendedDfs(null, internalNode, null, visitor, seenNodes,
internalNode.hashCode, i, 0);
}
else
{
this.dfs(null, internalNode, null, visitor, seenNodes, 0);
}
}
}
this.dfsCount++;
}
};
/**
* Function: dfs
*
* Performs a depth first search on the internal hierarchy model
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs a set of all of the
* ancestor node of the current node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxGraphHierarchyModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer)
{
if (root != null)
{
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i< outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.dfs(root, targetNode, internalEdge, visitor, seen,
layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Function: extendedDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of cells ancestors, but it
* should be only used when necessary because of it can be computationally
* intensive for deep searches.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs
* ancestors - the parent hash code
* childHash - the new hash code for this node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxGraphHierarchyModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer)
{
// Explanation of custom hash set. Previously, the ancestors variable
// was passed through the dfs as a HashSet. The ancestors were copied
// into a new HashSet and when the new child was processed it was also
// added to the set. If the current node was in its ancestor list it
// meant there is a cycle in the graph and this information is passed
// to the visitor.visit() in the seen parameter. The HashSet clone was
// very expensive on CPU so a custom hash was developed using primitive
// types. temp[] couldn't be used so hashCode[] was added to each node.
// Each new child adds another int to the array, copying the prefix
// from its parent. Child of the same parent add different ints (the
// limit is therefore 2^32 children per parent...). If a node has a
// child with the hashCode already set then the child code is compared
// to the same portion of the current nodes array. If they match there
// is a loop.
// Note that the basic mechanism would only allow for 1 use of this
// functionality, so the root nodes have two ints. The second int is
// incremented through each node root and the first is incremented
// through each run of the dfs algorithm (therefore the dfs is not
// thread safe). The hash code of each node is set if not already set,
// or if the first int does not match that of the current run.
if (root != null)
{
if (parent != null)
{
// Form this nodes hash code if necessary, that is, if the
// hashCode variable has not been initialized or if the
// start of the parent hash code does not equal the start of
// this nodes hash code, indicating the code was set on a
// previous run of this dfs.
if (root.hashCode == null ||
root.hashCode[0] != parent.hashCode[0])
{
var hashCodeLength = parent.hashCode.length + 1;
root.hashCode = parent.hashCode.slice();
root.hashCode[hashCodeLength - 1] = childHash;
}
}
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxSwimlaneModel
*
* Internal model of a hierarchical graph. This model stores nodes and edges
* equivalent to the real graph nodes and edges, but also stores the rank of the
* cells, the order within the ranks and the new candidate locations of cells.
* The internal model also reverses edge direction were appropriate , ignores
* self-loop and groups parallels together under one edge object.
*
* Constructor: mxSwimlaneModel
*
* Creates an internal ordered graph model using the vertices passed in. If
* there are any, leftward edge need to be inverted in the internal model
*
* Arguments:
*
* graph - the facade describing the graph to be operated on
* vertices - the vertices for this hierarchy
* ordered - whether or not the vertices are already ordered
* deterministic - whether or not this layout should be deterministic on each
* tightenToSource - whether or not to tighten vertices towards the sources
* scanRanksFromSinks - Whether rank assignment is from the sinks or sources.
* usage
*/
function mxSwimlaneModel(layout, vertices, roots, parent, tightenToSource)
{
var graph = layout.getGraph();
this.tightenToSource = tightenToSource;
this.roots = roots;
this.parent = parent;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly
this.vertexMapper = new mxDictionary();
this.edgeMapper = new mxDictionary();
this.maxRank = 0;
var internalVertices = [];
if (vertices == null)
{
vertices = this.graph.getChildVertices(parent);
}
this.maxRank = this.SOURCESCANSTARTRANK;
// map of cells to internal cell needed for second run through
// to setup the sink of edges correctly. Guess size by number
// of edges is roughly same as number of vertices.
this.createInternalCells(layout, vertices, internalVertices);
// Go through edges set their sink values. Also check the
// ordering if and invert edges if necessary
for (var i = 0; i < vertices.length; i++)
{
var edges = internalVertices[i].connectsAsSource;
for (var j = 0; j < edges.length; j++)
{
var internalEdge = edges[j];
var realEdges = internalEdge.edges;
// Only need to process the first real edge, since
// all the edges connect to the same other vertex
if (realEdges != null && realEdges.length > 0)
{
var realEdge = realEdges[0];
var targetCell = layout.getVisibleTerminal(
realEdge, false);
var internalTargetCell = this.vertexMapper.get(targetCell);
if (internalVertices[i] == internalTargetCell)
{
// If there are parallel edges going between two vertices and not all are in the same direction
// you can have navigated across one direction when doing the cycle reversal that isn't the same
// direction as the first real edge in the array above. When that happens the if above catches
// that and we correct the target cell before continuing.
// This branch only detects this single case
targetCell = layout.getVisibleTerminal(
realEdge, true);
internalTargetCell = this.vertexMapper.get(targetCell);
}
if (internalTargetCell != null
&& internalVertices[i] != internalTargetCell)
{
internalEdge.target = internalTargetCell;
if (internalTargetCell.connectsAsTarget.length == 0)
{
internalTargetCell.connectsAsTarget = [];
}
if (mxUtils.indexOf(internalTargetCell.connectsAsTarget, internalEdge) < 0)
{
internalTargetCell.connectsAsTarget.push(internalEdge);
}
}
}
}
// Use the temp variable in the internal nodes to mark this
// internal vertex as having been visited.
internalVertices[i].temp[0] = 1;
}
};
/**
* Variable: maxRank
*
* Stores the largest rank number allocated
*/
mxSwimlaneModel.prototype.maxRank = null;
/**
* Variable: vertexMapper
*
* Map from graph vertices to internal model nodes.
*/
mxSwimlaneModel.prototype.vertexMapper = null;
/**
* Variable: edgeMapper
*
* Map from graph edges to internal model edges
*/
mxSwimlaneModel.prototype.edgeMapper = null;
/**
* Variable: ranks
*
* Mapping from rank number to actual rank
*/
mxSwimlaneModel.prototype.ranks = null;
/**
* Variable: roots
*
* Store of roots of this hierarchy model, these are real graph cells, not
* internal cells
*/
mxSwimlaneModel.prototype.roots = null;
/**
* Variable: parent
*
* The parent cell whose children are being laid out
*/
mxSwimlaneModel.prototype.parent = null;
/**
* Variable: dfsCount
*
* Count of the number of times the ancestor dfs has been used.
*/
mxSwimlaneModel.prototype.dfsCount = 0;
/**
* Variable: SOURCESCANSTARTRANK
*
* High value to start source layering scan rank value from.
*/
mxSwimlaneModel.prototype.SOURCESCANSTARTRANK = 100000000;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells.
*/
mxSwimlaneModel.prototype.tightenToSource = false;
/**
* Variable: ranksPerGroup
*
* An array of the number of ranks within each swimlane
*/
mxSwimlaneModel.prototype.ranksPerGroup = null;
/**
* Function: createInternalCells
*
* Creates all edges in the internal model
*
* Parameters:
*
* layout - Reference to the <mxHierarchicalLayout> algorithm.
* vertices - Array of <mxCells> that represent the vertices whom are to
* have an internal representation created.
* internalVertices - The array of <mxGraphHierarchyNodes> to have their
* information filled in using the real vertices.
*/
mxSwimlaneModel.prototype.createInternalCells = function(layout, vertices, internalVertices)
{
var graph = layout.getGraph();
var swimlanes = layout.swimlanes;
// Create internal edges
for (var i = 0; i < vertices.length; i++)
{
internalVertices[i] = new mxGraphHierarchyNode(vertices[i]);
this.vertexMapper.put(vertices[i], internalVertices[i]);
internalVertices[i].swimlaneIndex = -1;
for (var ii = 0; ii < swimlanes.length; ii++)
{
if (graph.model.getParent(vertices[i]) == swimlanes[ii])
{
internalVertices[i].swimlaneIndex = ii;
break;
}
}
// If the layout is deterministic, order the cells
//List outgoingCells = graph.getNeighbours(vertices[i], deterministic);
var conns = layout.getEdges(vertices[i]);
internalVertices[i].connectsAsSource = [];
// Create internal edges, but don't do any rank assignment yet
// First use the information from the greedy cycle remover to
// invert the leftward edges internally
for (var j = 0; j < conns.length; j++)
{
var cell = layout.getVisibleTerminal(conns[j], false);
// Looking for outgoing edges only
if (cell != vertices[i] && layout.graph.model.isVertex(cell) &&
!layout.isVertexIgnored(cell))
{
// We process all edge between this source and its targets
// If there are edges going both ways, we need to collect
// them all into one internal edges to avoid looping problems
// later. We assume this direction (source -> target) is the
// natural direction if at least half the edges are going in
// that direction.
// The check below for edges[0] being in the vertex mapper is
// in case we've processed this the other way around
// (target -> source) and the number of edges in each direction
// are the same. All the graph edges will have been assigned to
// an internal edge going the other way, so we don't want to
// process them again
var undirectedEdges = layout.getEdgesBetween(vertices[i],
cell, false);
var directedEdges = layout.getEdgesBetween(vertices[i],
cell, true);
if (undirectedEdges != null &&
undirectedEdges.length > 0 &&
this.edgeMapper.get(undirectedEdges[0]) == null &&
directedEdges.length * 2 >= undirectedEdges.length)
{
var internalEdge = new mxGraphHierarchyEdge(undirectedEdges);
for (var k = 0; k < undirectedEdges.length; k++)
{
var edge = undirectedEdges[k];
this.edgeMapper.put(edge, internalEdge);
// Resets all point on the edge and disables the edge style
// without deleting it from the cell style
graph.resetEdge(edge);
if (layout.disableEdgeStyle)
{
layout.setEdgeStyleEnabled(edge, false);
layout.setOrthogonalEdge(edge,true);
}
}
internalEdge.source = internalVertices[i];
if (mxUtils.indexOf(internalVertices[i].connectsAsSource, internalEdge) < 0)
{
internalVertices[i].connectsAsSource.push(internalEdge);
}
}
}
}
// Ensure temp variable is cleared from any previous use
internalVertices[i].temp[0] = 0;
}
};
/**
* Function: initialRank
*
* Basic determination of minimum layer ranking by working from from sources
* or sinks and working through each node in the relevant edge direction.
* Starting at the sinks is basically a longest path layering algorithm.
*/
mxSwimlaneModel.prototype.initialRank = function()
{
this.ranksPerGroup = [];
var startNodes = [];
var seen = new Object();
if (this.roots != null)
{
for (var i = 0; i < this.roots.length; i++)
{
var internalNode = this.vertexMapper.get(this.roots[i]);
this.maxChainDfs(null, internalNode, null, seen, 0);
if (internalNode != null)
{
startNodes.push(internalNode);
}
}
}
// Calculate the lower and upper rank bounds of each swimlane
var lowerRank = [];
var upperRank = [];
for (var i = this.ranksPerGroup.length - 1; i >= 0; i--)
{
if (i == this.ranksPerGroup.length - 1)
{
lowerRank[i] = 0;
}
else
{
lowerRank[i] = upperRank[i+1] + 1;
}
upperRank[i] = lowerRank[i] + this.ranksPerGroup[i];
}
this.maxRank = upperRank[0];
var internalNodes = this.vertexMapper.getValues();
for (var i=0; i < internalNodes.length; i++)
{
// Mark the node as not having had a layer assigned
internalNodes[i].temp[0] = -1;
}
var startNodesCopy = startNodes.slice();
while (startNodes.length > 0)
{
var internalNode = startNodes[0];
var layerDeterminingEdges;
var edgesToBeMarked;
layerDeterminingEdges = internalNode.connectsAsTarget;
edgesToBeMarked = internalNode.connectsAsSource;
// flag to keep track of whether or not all layer determining
// edges have been scanned
var allEdgesScanned = true;
// Work out the layer of this node from the layer determining
// edges. The minimum layer number of any node connected by one of
// the layer determining edges variable
var minimumLayer = upperRank[0];
for (var i = 0; i < layerDeterminingEdges.length; i++)
{
var internalEdge = layerDeterminingEdges[i];
if (internalEdge.temp[0] == 5270620)
{
// This edge has been scanned, get the layer of the
// node on the other end
var otherNode = internalEdge.source;
minimumLayer = Math.min(minimumLayer, otherNode.temp[0] - 1);
}
else
{
allEdgesScanned = false;
break;
}
}
// If all edge have been scanned, assign the layer, mark all
// edges in the other direction and remove from the nodes list
if (allEdgesScanned)
{
if (minimumLayer > upperRank[internalNode.swimlaneIndex])
{
minimumLayer = upperRank[internalNode.swimlaneIndex];
}
internalNode.temp[0] = minimumLayer;
if (edgesToBeMarked != null)
{
for (var i = 0; i < edgesToBeMarked.length; i++)
{
var internalEdge = edgesToBeMarked[i];
// Assign unique stamp ( y/m/d/h )
internalEdge.temp[0] = 5270620;
// Add node on other end of edge to LinkedList of
// nodes to be analysed
var otherNode = internalEdge.target;
// Only add node if it hasn't been assigned a layer
if (otherNode.temp[0] == -1)
{
startNodes.push(otherNode);
// Mark this other node as neither being
// unassigned nor assigned so it isn't
// added to this list again, but it's
// layer isn't used in any calculation.
otherNode.temp[0] = -2;
}
}
}
startNodes.shift();
}
else
{
// Not all the edges have been scanned, get to the back of
// the class and put the dunces cap on
var removedCell = startNodes.shift();
startNodes.push(internalNode);
if (removedCell == internalNode && startNodes.length == 1)
{
// This is an error condition, we can't get out of
// this loop. It could happen for more than one node
// but that's a lot harder to detect. Log the error
// TODO make log comment
break;
}
}
}
// Normalize the ranks down from their large starting value to place
// at least 1 sink on layer 0
// for (var key in this.vertexMapper)
// {
// var internalNode = this.vertexMapper[key];
// // Mark the node as not having had a layer assigned
// internalNode.temp[0] -= this.maxRank;
// }
// Tighten the rank 0 nodes as far as possible
// for ( var i = 0; i < startNodesCopy.length; i++)
// {
// var internalNode = startNodesCopy[i];
// var currentMaxLayer = 0;
// var layerDeterminingEdges = internalNode.connectsAsSource;
//
// for ( var j = 0; j < layerDeterminingEdges.length; j++)
// {
// var internalEdge = layerDeterminingEdges[j];
// var otherNode = internalEdge.target;
// internalNode.temp[0] = Math.max(currentMaxLayer,
// otherNode.temp[0] + 1);
// currentMaxLayer = internalNode.temp[0];
// }
// }
};
/**
* Function: maxChainDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of chains within groups.
* Any cycles should be removed prior to running, but previously seen cells
* are ignored.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* seen - a set of all nodes seen by this dfs
* chainCount - the number of edges in the chain of vertices going through
* the current swimlane
*/
mxSwimlaneModel.prototype.maxChainDfs = function(parent, root, connectingEdge, seen, chainCount)
{
if (root != null)
{
var rootId = mxCellPath.create(root.cell);
if (seen[rootId] == null)
{
seen[rootId] = root;
var slIndex = root.swimlaneIndex;
if (this.ranksPerGroup[slIndex] == null || this.ranksPerGroup[slIndex] < chainCount)
{
this.ranksPerGroup[slIndex] = chainCount;
}
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Only navigate in source->target direction within the same
// swimlane, or from a lower index swimlane to a higher one
if (root.swimlaneIndex < targetNode.swimlaneIndex)
{
this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), 0);
}
else if (root.swimlaneIndex == targetNode.swimlaneIndex)
{
this.maxChainDfs(root, targetNode, internalEdge, mxUtils.clone(seen, null , true), chainCount + 1);
}
}
}
}
};
/**
* Function: fixRanks
*
* Fixes the layer assignments to the values stored in the nodes. Also needs
* to create dummy nodes for edges that cross layers.
*/
mxSwimlaneModel.prototype.fixRanks = function()
{
var rankList = [];
this.ranks = [];
for (var i = 0; i < this.maxRank + 1; i++)
{
rankList[i] = [];
this.ranks[i] = rankList[i];
}
// Perform a DFS to obtain an initial ordering for each rank.
// Without doing this you would end up having to process
// crossings for a standard tree.
var rootsArray = null;
if (this.roots != null)
{
var oldRootsArray = this.roots;
rootsArray = [];
for (var i = 0; i < oldRootsArray.length; i++)
{
var cell = oldRootsArray[i];
var internalNode = this.vertexMapper.get(cell);
rootsArray[i] = internalNode;
}
}
this.visit(function(parent, node, edge, layer, seen)
{
if (seen == 0 && node.maxRank < 0 && node.minRank < 0)
{
rankList[node.temp[0]].push(node);
node.maxRank = node.temp[0];
node.minRank = node.temp[0];
// Set temp[0] to the nodes position in the rank
node.temp[0] = rankList[node.maxRank].length - 1;
}
if (parent != null && edge != null)
{
var parentToCellRankDifference = parent.maxRank - node.maxRank;
if (parentToCellRankDifference > 1)
{
// There are ranks in between the parent and current cell
edge.maxRank = parent.maxRank;
edge.minRank = node.maxRank;
edge.temp = [];
edge.x = [];
edge.y = [];
for (var i = edge.minRank + 1; i < edge.maxRank; i++)
{
// The connecting edge must be added to the
// appropriate ranks
rankList[i].push(edge);
edge.setGeneralPurposeVariable(i, rankList[i]
.length - 1);
}
}
}
}, rootsArray, false, null);
};
/**
* Function: visit
*
* A depth first search through the internal heirarchy model.
*
* Parameters:
*
* visitor - The visitor function pattern to be called for each node.
* trackAncestors - Whether or not the search is to keep track all nodes
* directly above this one in the search path.
*/
mxSwimlaneModel.prototype.visit = function(visitor, dfsRoots, trackAncestors, seenNodes)
{
// Run dfs through on all roots
if (dfsRoots != null)
{
for (var i = 0; i < dfsRoots.length; i++)
{
var internalNode = dfsRoots[i];
if (internalNode != null)
{
if (seenNodes == null)
{
seenNodes = new Object();
}
if (trackAncestors)
{
// Set up hash code for root
internalNode.hashCode = [];
internalNode.hashCode[0] = this.dfsCount;
internalNode.hashCode[1] = i;
this.extendedDfs(null, internalNode, null, visitor, seenNodes,
internalNode.hashCode, i, 0);
}
else
{
this.dfs(null, internalNode, null, visitor, seenNodes, 0);
}
}
}
this.dfsCount++;
}
};
/**
* Function: dfs
*
* Performs a depth first search on the internal hierarchy model
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs a set of all of the
* ancestor node of the current node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxSwimlaneModel.prototype.dfs = function(parent, root, connectingEdge, visitor, seen, layer)
{
if (root != null)
{
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
for (var i = 0; i< outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Root check is O(|roots|)
this.dfs(root, targetNode, internalEdge, visitor, seen,
layer + 1);
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Function: extendedDfs
*
* Performs a depth first search on the internal hierarchy model. This dfs
* extends the default version by keeping track of cells ancestors, but it
* should be only used when necessary because of it can be computationally
* intensive for deep searches.
*
* Parameters:
*
* parent - the parent internal node of the current internal node
* root - the current internal node
* connectingEdge - the internal edge connecting the internal node and the parent
* internal node, if any
* visitor - the visitor pattern to be called for each node
* seen - a set of all nodes seen by this dfs
* ancestors - the parent hash code
* childHash - the new hash code for this node
* layer - the layer on the dfs tree ( not the same as the model ranks )
*/
mxSwimlaneModel.prototype.extendedDfs = function(parent, root, connectingEdge, visitor, seen, ancestors, childHash, layer)
{
// Explanation of custom hash set. Previously, the ancestors variable
// was passed through the dfs as a HashSet. The ancestors were copied
// into a new HashSet and when the new child was processed it was also
// added to the set. If the current node was in its ancestor list it
// meant there is a cycle in the graph and this information is passed
// to the visitor.visit() in the seen parameter. The HashSet clone was
// very expensive on CPU so a custom hash was developed using primitive
// types. temp[] couldn't be used so hashCode[] was added to each node.
// Each new child adds another int to the array, copying the prefix
// from its parent. Child of the same parent add different ints (the
// limit is therefore 2^32 children per parent...). If a node has a
// child with the hashCode already set then the child code is compared
// to the same portion of the current nodes array. If they match there
// is a loop.
// Note that the basic mechanism would only allow for 1 use of this
// functionality, so the root nodes have two ints. The second int is
// incremented through each node root and the first is incremented
// through each run of the dfs algorithm (therefore the dfs is not
// thread safe). The hash code of each node is set if not already set,
// or if the first int does not match that of the current run.
if (root != null)
{
if (parent != null)
{
// Form this nodes hash code if necessary, that is, if the
// hashCode variable has not been initialized or if the
// start of the parent hash code does not equal the start of
// this nodes hash code, indicating the code was set on a
// previous run of this dfs.
if (root.hashCode == null ||
root.hashCode[0] != parent.hashCode[0])
{
var hashCodeLength = parent.hashCode.length + 1;
root.hashCode = parent.hashCode.slice();
root.hashCode[hashCodeLength - 1] = childHash;
}
}
var rootId = root.id;
if (seen[rootId] == null)
{
seen[rootId] = root;
visitor(parent, root, connectingEdge, layer, 0);
// Copy the connects as source list so that visitors
// can change the original for edge direction inversions
var outgoingEdges = root.connectsAsSource.slice();
var incomingEdges = root.connectsAsTarget.slice();
for (var i = 0; i < outgoingEdges.length; i++)
{
var internalEdge = outgoingEdges[i];
var targetNode = internalEdge.target;
// Only navigate in source->target direction within the same
// swimlane, or from a lower index swimlane to a higher one
if (root.swimlaneIndex <= targetNode.swimlaneIndex)
{
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
for (var i = 0; i < incomingEdges.length; i++)
{
var internalEdge = incomingEdges[i];
var targetNode = internalEdge.source;
// Only navigate in target->source direction from a lower index
// swimlane to a higher one
if (root.swimlaneIndex < targetNode.swimlaneIndex)
{
this.extendedDfs(root, targetNode, internalEdge, visitor, seen,
root.hashCode, i, layer + 1);
}
}
}
else
{
// Use the int field to indicate this node has been seen
visitor(parent, root, connectingEdge, layer, 1);
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxHierarchicalLayoutStage
*
* The specific layout interface for hierarchical layouts. It adds a
* <code>run</code> method with a parameter for the hierarchical layout model
* that is shared between the layout stages.
*
* Constructor: mxHierarchicalLayoutStage
*
* Constructs a new hierarchical layout stage.
*/
function mxHierarchicalLayoutStage() { };
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxHierarchicalLayoutStage.prototype.execute = function(parent) { };
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMedianHybridCrossingReduction
*
* Sets the horizontal locations of node and edge dummy nodes on each layer.
* Uses median down and up weighings as well heuristic to straighten edges as
* far as possible.
*
* Constructor: mxMedianHybridCrossingReduction
*
* Creates a coordinate assignment.
*
* Arguments:
*
* intraCellSpacing - the minimum buffer between cells on the same rank
* interRankCellSpacing - the minimum distance between cells on adjacent ranks
* orientation - the position of the root node(s) relative to the graph
* initialX - the leftmost coordinate node placement starts at
*/
function mxMedianHybridCrossingReduction(layout)
{
this.layout = layout;
};
/**
* Extends mxMedianHybridCrossingReduction.
*/
mxMedianHybridCrossingReduction.prototype = new mxHierarchicalLayoutStage();
mxMedianHybridCrossingReduction.prototype.constructor = mxMedianHybridCrossingReduction;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxMedianHybridCrossingReduction.prototype.layout = null;
/**
* Variable: maxIterations
*
* The maximum number of iterations to perform whilst reducing edge
* crossings. Default is 24.
*/
mxMedianHybridCrossingReduction.prototype.maxIterations = 24;
/**
* Variable: nestedBestRanks
*
* Stores each rank as a collection of cells in the best order found for
* each layer so far
*/
mxMedianHybridCrossingReduction.prototype.nestedBestRanks = null;
/**
* Variable: currentBestCrossings
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.currentBestCrossings = 0;
/**
* Variable: iterationsWithoutImprovement
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.iterationsWithoutImprovement = 0;
/**
* Variable: maxNoImprovementIterations
*
* The total number of crossings found in the best configuration so far
*/
mxMedianHybridCrossingReduction.prototype.maxNoImprovementIterations = 2;
/**
* Function: execute
*
* Performs a vertex ordering within ranks as described by Gansner et al
* 1993
*/
mxMedianHybridCrossingReduction.prototype.execute = function(parent)
{
var model = this.layout.getModel();
// Stores initial ordering as being the best one found so far
this.nestedBestRanks = [];
for (var i = 0; i < model.ranks.length; i++)
{
this.nestedBestRanks[i] = model.ranks[i].slice();
}
var iterationsWithoutImprovement = 0;
var currentBestCrossings = this.calculateCrossings(model);
for (var i = 0; i < this.maxIterations &&
iterationsWithoutImprovement < this.maxNoImprovementIterations; i++)
{
this.weightedMedian(i, model);
this.transpose(i, model);
var candidateCrossings = this.calculateCrossings(model);
if (candidateCrossings < currentBestCrossings)
{
currentBestCrossings = candidateCrossings;
iterationsWithoutImprovement = 0;
// Store the current rankings as the best ones
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
this.nestedBestRanks[j][cell.getGeneralPurposeVariable(j)] = cell;
}
}
}
else
{
// Increase count of iterations where we haven't improved the
// layout
iterationsWithoutImprovement++;
// Restore the best values to the cells
for (var j = 0; j < this.nestedBestRanks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
cell.setGeneralPurposeVariable(j, k);
}
}
}
if (currentBestCrossings == 0)
{
// Do nothing further
break;
}
}
// Store the best rankings but in the model
var ranks = [];
var rankList = [];
for (var i = 0; i < model.maxRank + 1; i++)
{
rankList[i] = [];
ranks[i] = rankList[i];
}
for (var i = 0; i < this.nestedBestRanks.length; i++)
{
for (var j = 0; j < this.nestedBestRanks[i].length; j++)
{
rankList[i].push(this.nestedBestRanks[i][j]);
}
}
model.ranks = ranks;
};
/**
* Function: calculateCrossings
*
* Calculates the total number of edge crossing in the current graph.
* Returns the current number of edge crossings in the hierarchy graph
* model in the current candidate layout
*
* Parameters:
*
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateCrossings = function(model)
{
var numRanks = model.ranks.length;
var totalCrossings = 0;
for (var i = 1; i < numRanks; i++)
{
totalCrossings += this.calculateRankCrossing(i, model);
}
return totalCrossings;
};
/**
* Function: calculateRankCrossing
*
* Calculates the number of edges crossings between the specified rank and
* the rank below it. Returns the number of edges crossings with the rank
* beneath
*
* Parameters:
*
* i - the topmost rank of the pair ( higher rank value )
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.calculateRankCrossing = function(i, model)
{
var totalCrossings = 0;
var rank = model.ranks[i];
var previousRank = model.ranks[i - 1];
var tmpIndices = [];
// Iterate over the top rank and fill in the connection information
for (var j = 0; j < rank.length; j++)
{
var node = rank[j];
var rankPosition = node.getGeneralPurposeVariable(i);
var connectedCells = node.getPreviousLayerConnectedCells(i);
var nodeIndices = [];
for (var k = 0; k < connectedCells.length; k++)
{
var connectedNode = connectedCells[k];
var otherCellRankPosition = connectedNode.getGeneralPurposeVariable(i - 1);
nodeIndices.push(otherCellRankPosition);
}
nodeIndices.sort(function(x, y) { return x - y; });
tmpIndices[rankPosition] = nodeIndices;
}
var indices = [];
for (var j = 0; j < tmpIndices.length; j++)
{
indices = indices.concat(tmpIndices[j]);
}
var firstIndex = 1;
while (firstIndex < previousRank.length)
{
firstIndex <<= 1;
}
var treeSize = 2 * firstIndex - 1;
firstIndex -= 1;
var tree = [];
for (var j = 0; j < treeSize; ++j)
{
tree[j] = 0;
}
for (var j = 0; j < indices.length; j++)
{
var index = indices[j];
var treeIndex = index + firstIndex;
++tree[treeIndex];
while (treeIndex > 0)
{
if (treeIndex % 2)
{
totalCrossings += tree[treeIndex + 1];
}
treeIndex = (treeIndex - 1) >> 1;
++tree[treeIndex];
}
}
return totalCrossings;
};
/**
* Function: transpose
*
* Takes each possible adjacent cell pair on each rank and checks if
* swapping them around reduces the number of crossing
*
* Parameters:
*
* mainLoopIteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.transpose = function(mainLoopIteration, model)
{
var improved = true;
// Track the number of iterations in case of looping
var count = 0;
var maxCount = 10;
while (improved && count++ < maxCount)
{
// On certain iterations allow allow swapping of cell pairs with
// equal edge crossings switched or not switched. This help to
// nudge a stuck layout into a lower crossing total.
var nudge = mainLoopIteration % 2 == 1 && count % 2 == 1;
improved = false;
for (var i = 0; i < model.ranks.length; i++)
{
var rank = model.ranks[i];
var orderedCells = [];
for (var j = 0; j < rank.length; j++)
{
var cell = rank[j];
var tempRank = cell.getGeneralPurposeVariable(i);
// FIXME: Workaround to avoid negative tempRanks
if (tempRank < 0)
{
tempRank = j;
}
orderedCells[tempRank] = cell;
}
var leftCellAboveConnections = null;
var leftCellBelowConnections = null;
var rightCellAboveConnections = null;
var rightCellBelowConnections = null;
var leftAbovePositions = null;
var leftBelowPositions = null;
var rightAbovePositions = null;
var rightBelowPositions = null;
var leftCell = null;
var rightCell = null;
for (var j = 0; j < (rank.length - 1); j++)
{
// For each intra-rank adjacent pair of cells
// see if swapping them around would reduce the
// number of edges crossing they cause in total
// On every cell pair except the first on each rank, we
// can save processing using the previous values for the
// right cell on the new left cell
if (j == 0)
{
leftCell = orderedCells[j];
leftCellAboveConnections = leftCell
.getNextLayerConnectedCells(i);
leftCellBelowConnections = leftCell
.getPreviousLayerConnectedCells(i);
leftAbovePositions = [];
leftBelowPositions = [];
for (var k = 0; k < leftCellAboveConnections.length; k++)
{
leftAbovePositions[k] = leftCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < leftCellBelowConnections.length; k++)
{
leftBelowPositions[k] = leftCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
}
else
{
leftCellAboveConnections = rightCellAboveConnections;
leftCellBelowConnections = rightCellBelowConnections;
leftAbovePositions = rightAbovePositions;
leftBelowPositions = rightBelowPositions;
leftCell = rightCell;
}
rightCell = orderedCells[j + 1];
rightCellAboveConnections = rightCell
.getNextLayerConnectedCells(i);
rightCellBelowConnections = rightCell
.getPreviousLayerConnectedCells(i);
rightAbovePositions = [];
rightBelowPositions = [];
for (var k = 0; k < rightCellAboveConnections.length; k++)
{
rightAbovePositions[k] = rightCellAboveConnections[k].getGeneralPurposeVariable(i + 1);
}
for (var k = 0; k < rightCellBelowConnections.length; k++)
{
rightBelowPositions[k] = rightCellBelowConnections[k].getGeneralPurposeVariable(i - 1);
}
var totalCurrentCrossings = 0;
var totalSwitchedCrossings = 0;
for (var k = 0; k < leftAbovePositions.length; k++)
{
for (var ik = 0; ik < rightAbovePositions.length; ik++)
{
if (leftAbovePositions[k] > rightAbovePositions[ik])
{
totalCurrentCrossings++;
}
if (leftAbovePositions[k] < rightAbovePositions[ik])
{
totalSwitchedCrossings++;
}
}
}
for (var k = 0; k < leftBelowPositions.length; k++)
{
for (var ik = 0; ik < rightBelowPositions.length; ik++)
{
if (leftBelowPositions[k] > rightBelowPositions[ik])
{
totalCurrentCrossings++;
}
if (leftBelowPositions[k] < rightBelowPositions[ik])
{
totalSwitchedCrossings++;
}
}
}
if ((totalSwitchedCrossings < totalCurrentCrossings) ||
(totalSwitchedCrossings == totalCurrentCrossings &&
nudge))
{
var temp = leftCell.getGeneralPurposeVariable(i);
leftCell.setGeneralPurposeVariable(i, rightCell
.getGeneralPurposeVariable(i));
rightCell.setGeneralPurposeVariable(i, temp);
// With this pair exchanged we have to switch all of
// values for the left cell to the right cell so the
// next iteration for this rank uses it as the left
// cell again
rightCellAboveConnections = leftCellAboveConnections;
rightCellBelowConnections = leftCellBelowConnections;
rightAbovePositions = leftAbovePositions;
rightBelowPositions = leftBelowPositions;
rightCell = leftCell;
if (!nudge)
{
// Don't count nudges as improvement or we'll end
// up stuck in two combinations and not finishing
// as early as we should
improved = true;
}
}
}
}
}
};
/**
* Function: weightedMedian
*
* Sweeps up or down the layout attempting to minimise the median placement
* of connected cells on adjacent ranks
*
* Parameters:
*
* iteration - the iteration number of the main loop
* model - the internal model describing the hierarchy
*/
mxMedianHybridCrossingReduction.prototype.weightedMedian = function(iteration, model)
{
// Reverse sweep direction each time through this method
var downwardSweep = (iteration % 2 == 0);
if (downwardSweep)
{
for (var j = model.maxRank - 1; j >= 0; j--)
{
this.medianRank(j, downwardSweep);
}
}
else
{
for (var j = 1; j < model.maxRank; j++)
{
this.medianRank(j, downwardSweep);
}
}
};
/**
* Function: medianRank
*
* Attempts to minimise the median placement of connected cells on this rank
* and one of the adjacent ranks
*
* Parameters:
*
* rankValue - the layer number of this rank
* downwardSweep - whether or not this is a downward sweep through the graph
*/
mxMedianHybridCrossingReduction.prototype.medianRank = function(rankValue, downwardSweep)
{
var numCellsForRank = this.nestedBestRanks[rankValue].length;
var medianValues = [];
var reservedPositions = [];
for (var i = 0; i < numCellsForRank; i++)
{
var cell = this.nestedBestRanks[rankValue][i];
var sorterEntry = new MedianCellSorter();
sorterEntry.cell = cell;
// Flip whether or not equal medians are flipped on up and down
// sweeps
// TODO re-implement some kind of nudge
// medianValues[i].nudge = !downwardSweep;
var nextLevelConnectedCells;
if (downwardSweep)
{
nextLevelConnectedCells = cell
.getNextLayerConnectedCells(rankValue);
}
else
{
nextLevelConnectedCells = cell
.getPreviousLayerConnectedCells(rankValue);
}
var nextRankValue;
if (downwardSweep)
{
nextRankValue = rankValue + 1;
}
else
{
nextRankValue = rankValue - 1;
}
if (nextLevelConnectedCells != null
&& nextLevelConnectedCells.length != 0)
{
sorterEntry.medianValue = this.medianValue(
nextLevelConnectedCells, nextRankValue);
medianValues.push(sorterEntry);
}
else
{
// Nodes with no adjacent vertices are flagged in the reserved array
// to indicate they should be left in their current position.
reservedPositions[cell.getGeneralPurposeVariable(rankValue)] = true;
}
}
medianValues.sort(MedianCellSorter.prototype.compare);
// Set the new position of each node within the rank using
// its temp variable
for (var i = 0; i < numCellsForRank; i++)
{
if (reservedPositions[i] == null)
{
var cell = medianValues.shift().cell;
cell.setGeneralPurposeVariable(rankValue, i);
}
}
};
/**
* Function: medianValue
*
* Calculates the median rank order positioning for the specified cell using
* the connected cells on the specified rank. Returns the median rank
* ordering value of the connected cells
*
* Parameters:
*
* connectedCells - the cells on the specified rank connected to the
* specified cell
* rankValue - the rank that the connected cell lie upon
*/
mxMedianHybridCrossingReduction.prototype.medianValue = function(connectedCells, rankValue)
{
var medianValues = [];
var arrayCount = 0;
for (var i = 0; i < connectedCells.length; i++)
{
var cell = connectedCells[i];
medianValues[arrayCount++] = cell.getGeneralPurposeVariable(rankValue);
}
// Sort() sorts lexicographically by default (i.e. 11 before 9) so force
// numerical order sort
medianValues.sort(function(a,b){return a - b;});
if (arrayCount % 2 == 1)
{
// For odd numbers of adjacent vertices return the median
return medianValues[Math.floor(arrayCount / 2)];
}
else if (arrayCount == 2)
{
return ((medianValues[0] + medianValues[1]) / 2.0);
}
else
{
var medianPoint = arrayCount / 2;
var leftMedian = medianValues[medianPoint - 1] - medianValues[0];
var rightMedian = medianValues[arrayCount - 1]
- medianValues[medianPoint];
return (medianValues[medianPoint - 1] * rightMedian + medianValues[medianPoint]
* leftMedian)
/ (leftMedian + rightMedian);
}
};
/**
* Class: MedianCellSorter
*
* A utility class used to track cells whilst sorting occurs on the median
* values. Does not violate (x.compareTo(y)==0) == (x.equals(y))
*
* Constructor: MedianCellSorter
*
* Constructs a new median cell sorter.
*/
function MedianCellSorter()
{
// empty
};
/**
* Variable: medianValue
*
* The weighted value of the cell stored.
*/
MedianCellSorter.prototype.medianValue = 0;
/**
* Variable: cell
*
* The cell whose median value is being calculated
*/
MedianCellSorter.prototype.cell = false;
/**
* Function: compare
*
* Compares two MedianCellSorters.
*/
MedianCellSorter.prototype.compare = function(a, b)
{
if (a != null && b != null)
{
if (b.medianValue > a.medianValue)
{
return -1;
}
else if (b.medianValue < a.medianValue)
{
return 1;
}
else
{
return 0;
}
}
else
{
return 0;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMinimumCycleRemover
*
* An implementation of the first stage of the Sugiyama layout. Straightforward
* longest path calculation of layer assignment
*
* Constructor: mxMinimumCycleRemover
*
* Creates a cycle remover for the given internal model.
*/
function mxMinimumCycleRemover(layout)
{
this.layout = layout;
};
/**
* Extends mxHierarchicalLayoutStage.
*/
mxMinimumCycleRemover.prototype = new mxHierarchicalLayoutStage();
mxMinimumCycleRemover.prototype.constructor = mxMinimumCycleRemover;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxMinimumCycleRemover.prototype.layout = null;
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxMinimumCycleRemover.prototype.execute = function(parent)
{
var model = this.layout.getModel();
var seenNodes = new Object();
var unseenNodesArray = model.vertexMapper.getValues();
var unseenNodes = new Object();
for (var i = 0; i < unseenNodesArray.length; i++)
{
unseenNodes[unseenNodesArray[i].id] = unseenNodesArray[i];
}
// Perform a dfs through the internal model. If a cycle is found,
// reverse it.
var rootsArray = null;
if (model.roots != null)
{
var modelRoots = model.roots;
rootsArray = [];
for (var i = 0; i < modelRoots.length; i++)
{
rootsArray[i] = model.vertexMapper.get(modelRoots[i]);
}
}
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
if (node.isAncestor(parent))
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
node.connectsAsSource.push(connectingEdge);
}
seenNodes[node.id] = node;
delete unseenNodes[node.id];
}, rootsArray, true, null);
// If there are any nodes that should be nodes that the dfs can miss
// these need to be processed with the dfs and the roots assigned
// correctly to form a correct internal model
var seenNodesCopy = mxUtils.clone(seenNodes, null, true);
// Pick a random cell and dfs from it
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
if (node.isAncestor(parent))
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
node.connectsAsSource.push(connectingEdge);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
}
seenNodes[node.id] = node;
delete unseenNodes[node.id];
}, unseenNodes, true, seenNodesCopy);
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxCoordinateAssignment
*
* Sets the horizontal locations of node and edge dummy nodes on each layer.
* Uses median down and up weighings as well as heuristics to straighten edges as
* far as possible.
*
* Constructor: mxCoordinateAssignment
*
* Creates a coordinate assignment.
*
* Arguments:
*
* intraCellSpacing - the minimum buffer between cells on the same rank
* interRankCellSpacing - the minimum distance between cells on adjacent ranks
* orientation - the position of the root node(s) relative to the graph
* initialX - the leftmost coordinate node placement starts at
*/
function mxCoordinateAssignment(layout, intraCellSpacing, interRankCellSpacing,
orientation, initialX, parallelEdgeSpacing)
{
this.layout = layout;
this.intraCellSpacing = intraCellSpacing;
this.interRankCellSpacing = interRankCellSpacing;
this.orientation = orientation;
this.initialX = initialX;
this.parallelEdgeSpacing = parallelEdgeSpacing;
};
/**
* Extends mxHierarchicalLayoutStage.
*/
mxCoordinateAssignment.prototype = new mxHierarchicalLayoutStage();
mxCoordinateAssignment.prototype.constructor = mxCoordinateAssignment;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxCoordinateAssignment.prototype.layout = null;
/**
* Variable: intraCellSpacing
*
* The minimum buffer between cells on the same rank. Default is 30.
*/
mxCoordinateAssignment.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The minimum distance between cells on adjacent ranks. Default is 100.
*/
mxCoordinateAssignment.prototype.interRankCellSpacing = 100;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges.
* Default is 10.
*/
mxCoordinateAssignment.prototype.parallelEdgeSpacing = 10;
/**
* Variable: maxIterations
*
* The number of heuristic iterations to run. Default is 8.
*/
mxCoordinateAssignment.prototype.maxIterations = 8;
/**
* Variable: prefHozEdgeSep
*
* The preferred horizontal distance between edges exiting a vertex Default is 5.
*/
mxCoordinateAssignment.prototype.prefHozEdgeSep = 5;
/**
* Variable: prefVertEdgeOff
*
* The preferred vertical offset between edges exiting a vertex Default is 2.
*/
mxCoordinateAssignment.prototype.prefVertEdgeOff = 2;
/**
* Variable: minEdgeJetty
*
* The minimum distance for an edge jetty from a vertex Default is 12.
*/
mxCoordinateAssignment.prototype.minEdgeJetty = 12;
/**
* Variable: channelBuffer
*
* The size of the vertical buffer in the center of inter-rank channels
* where edge control points should not be placed Default is 4.
*/
mxCoordinateAssignment.prototype.channelBuffer = 4;
/**
* Variable: jettyPositions
*
* Map of internal edges and (x,y) pair of positions of the start and end jetty
* for that edge where it connects to the source and target vertices.
* Note this should technically be a WeakHashMap, but since JS does not
* have an equivalent, housekeeping must be performed before using.
* i.e. check all edges are still in the model and clear the values.
* Note that the y co-ord is the offset of the jetty, not the
* absolute point
*/
mxCoordinateAssignment.prototype.jettyPositions = null;
/**
* Variable: orientation
*
* The position of the root ( start ) node(s) relative to the rest of the
* laid out graph. Default is <mxConstants.DIRECTION_NORTH>.
*/
mxCoordinateAssignment.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: initialX
*
* The minimum x position node placement starts at
*/
mxCoordinateAssignment.prototype.initialX = null;
/**
* Variable: limitX
*
* The maximum x value this positioning lays up to
*/
mxCoordinateAssignment.prototype.limitX = null;
/**
* Variable: currentXDelta
*
* The sum of x-displacements for the current iteration
*/
mxCoordinateAssignment.prototype.currentXDelta = null;
/**
* Variable: widestRank
*
* The rank that has the widest x position
*/
mxCoordinateAssignment.prototype.widestRank = null;
/**
* Variable: rankTopY
*
* Internal cache of top-most values of Y for each rank
*/
mxCoordinateAssignment.prototype.rankTopY = null;
/**
* Variable: rankBottomY
*
* Internal cache of bottom-most value of Y for each rank
*/
mxCoordinateAssignment.prototype.rankBottomY = null;
/**
* Variable: widestRankValue
*
* The X-coordinate of the edge of the widest rank
*/
mxCoordinateAssignment.prototype.widestRankValue = null;
/**
* Variable: rankWidths
*
* The width of all the ranks
*/
mxCoordinateAssignment.prototype.rankWidths = null;
/**
* Variable: rankY
*
* The Y-coordinate of all the ranks
*/
mxCoordinateAssignment.prototype.rankY = null;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxCoordinateAssignment.prototype.fineTuning = true;
/**
* Variable: nextLayerConnectedCache
*
* A store of connections to the layer above for speed
*/
mxCoordinateAssignment.prototype.nextLayerConnectedCache = null;
/**
* Variable: previousLayerConnectedCache
*
* A store of connections to the layer below for speed
*/
mxCoordinateAssignment.prototype.previousLayerConnectedCache = null;
/**
* Variable: groupPadding
*
* Padding added to resized parents Default is 10.
*/
mxCoordinateAssignment.prototype.groupPadding = 10;
/**
* Utility method to display current positions
*/
mxCoordinateAssignment.prototype.printStatus = function()
{
var model = this.layout.getModel();
mxLog.show();
mxLog.writeln('======Coord assignment debug=======');
for (var j = 0; j < model.ranks.length; j++)
{
mxLog.write('Rank ', j, ' : ' );
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
mxLog.write(cell.getGeneralPurposeVariable(j), ' ');
}
mxLog.writeln();
}
mxLog.writeln('====================================');
};
/**
* Function: execute
*
* A basic horizontal coordinate assignment algorithm
*/
mxCoordinateAssignment.prototype.execute = function(parent)
{
this.jettyPositions = Object();
var model = this.layout.getModel();
this.currentXDelta = 0.0;
this.initialCoords(this.layout.getGraph(), model);
// this.printStatus();
if (this.fineTuning)
{
this.minNode(model);
}
var bestXDelta = 100000000.0;
if (this.fineTuning)
{
for (var i = 0; i < this.maxIterations; i++)
{
// this.printStatus();
// Median Heuristic
if (i != 0)
{
this.medianPos(i, model);
this.minNode(model);
}
// if the total offset is less for the current positioning,
// there are less heavily angled edges and so the current
// positioning is used
if (this.currentXDelta < bestXDelta)
{
for (var j = 0; j < model.ranks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
cell.setX(j, cell.getGeneralPurposeVariable(j));
}
}
bestXDelta = this.currentXDelta;
}
else
{
// Restore the best positions
for (var j = 0; j < model.ranks.length; j++)
{
var rank = model.ranks[j];
for (var k = 0; k < rank.length; k++)
{
var cell = rank[k];
cell.setGeneralPurposeVariable(j, cell.getX(j));
}
}
}
this.minPath(this.layout.getGraph(), model);
this.currentXDelta = 0;
}
}
this.setCellLocations(this.layout.getGraph(), model);
};
/**
* Function: minNode
*
* Performs one median positioning sweep in both directions
*/
mxCoordinateAssignment.prototype.minNode = function(model)
{
// Queue all nodes
var nodeList = [];
// Need to be able to map from cell to cellWrapper
var map = new mxDictionary();
var rank = [];
for (var i = 0; i <= model.maxRank; i++)
{
rank[i] = model.ranks[i];
for (var j = 0; j < rank[i].length; j++)
{
// Use the weight to store the rank and visited to store whether
// or not the cell is in the list
var node = rank[i][j];
var nodeWrapper = new WeightedCellSorter(node, i);
nodeWrapper.rankIndex = j;
nodeWrapper.visited = true;
nodeList.push(nodeWrapper);
map.put(node, nodeWrapper);
}
}
// Set a limit of the maximum number of times we will access the queue
// in case a loop appears
var maxTries = nodeList.length * 10;
var count = 0;
// Don't move cell within this value of their median
var tolerance = 1;
while (nodeList.length > 0 && count <= maxTries)
{
var cellWrapper = nodeList.shift();
var cell = cellWrapper.cell;
var rankValue = cellWrapper.weightedValue;
var rankIndex = parseInt(cellWrapper.rankIndex);
var nextLayerConnectedCells = cell.getNextLayerConnectedCells(rankValue);
var previousLayerConnectedCells = cell.getPreviousLayerConnectedCells(rankValue);
var numNextLayerConnected = nextLayerConnectedCells.length;
var numPreviousLayerConnected = previousLayerConnectedCells.length;
var medianNextLevel = this.medianXValue(nextLayerConnectedCells,
rankValue + 1);
var medianPreviousLevel = this.medianXValue(previousLayerConnectedCells,
rankValue - 1);
var numConnectedNeighbours = numNextLayerConnected
+ numPreviousLayerConnected;
var currentPosition = cell.getGeneralPurposeVariable(rankValue);
var cellMedian = currentPosition;
if (numConnectedNeighbours > 0)
{
cellMedian = (medianNextLevel * numNextLayerConnected + medianPreviousLevel
* numPreviousLayerConnected)
/ numConnectedNeighbours;
}
// Flag storing whether or not position has changed
var positionChanged = false;
if (cellMedian < currentPosition - tolerance)
{
if (rankIndex == 0)
{
cell.setGeneralPurposeVariable(rankValue, cellMedian);
positionChanged = true;
}
else
{
var leftCell = rank[rankValue][rankIndex - 1];
var leftLimit = leftCell
.getGeneralPurposeVariable(rankValue);
leftLimit = leftLimit + leftCell.width / 2
+ this.intraCellSpacing + cell.width / 2;
if (leftLimit < cellMedian)
{
cell.setGeneralPurposeVariable(rankValue, cellMedian);
positionChanged = true;
}
else if (leftLimit < cell
.getGeneralPurposeVariable(rankValue)
- tolerance)
{
cell.setGeneralPurposeVariable(rankValue, leftLimit);
positionChanged = true;
}
}
}
else if (cellMedian > currentPosition + tolerance)
{
var rankSize = rank[rankValue].length;
if (rankIndex == rankSize - 1)
{
cell.setGeneralPurposeVariable(rankValue, cellMedian);
positionChanged = true;
}
else
{
var rightCell = rank[rankValue][rankIndex + 1];
var rightLimit = rightCell
.getGeneralPurposeVariable(rankValue);
rightLimit = rightLimit - rightCell.width / 2
- this.intraCellSpacing - cell.width / 2;
if (rightLimit > cellMedian)
{
cell.setGeneralPurposeVariable(rankValue, cellMedian);
positionChanged = true;
}
else if (rightLimit > cell
.getGeneralPurposeVariable(rankValue)
+ tolerance)
{
cell.setGeneralPurposeVariable(rankValue, rightLimit);
positionChanged = true;
}
}
}
if (positionChanged)
{
// Add connected nodes to map and list
for (var i = 0; i < nextLayerConnectedCells.length; i++)
{
var connectedCell = nextLayerConnectedCells[i];
var connectedCellWrapper = map.get(connectedCell);
if (connectedCellWrapper != null)
{
if (connectedCellWrapper.visited == false)
{
connectedCellWrapper.visited = true;
nodeList.push(connectedCellWrapper);
}
}
}
// Add connected nodes to map and list
for (var i = 0; i < previousLayerConnectedCells.length; i++)
{
var connectedCell = previousLayerConnectedCells[i];
var connectedCellWrapper = map.get(connectedCell);
if (connectedCellWrapper != null)
{
if (connectedCellWrapper.visited == false)
{
connectedCellWrapper.visited = true;
nodeList.push(connectedCellWrapper);
}
}
}
}
cellWrapper.visited = false;
count++;
}
};
/**
* Function: medianPos
*
* Performs one median positioning sweep in one direction
*
* Parameters:
*
* i - the iteration of the whole process
* model - an internal model of the hierarchical layout
*/
mxCoordinateAssignment.prototype.medianPos = function(i, model)
{
// Reverse sweep direction each time through this method
var downwardSweep = (i % 2 == 0);
if (downwardSweep)
{
for (var j = model.maxRank; j > 0; j--)
{
this.rankMedianPosition(j - 1, model, j);
}
}
else
{
for (var j = 0; j < model.maxRank - 1; j++)
{
this.rankMedianPosition(j + 1, model, j);
}
}
};
/**
* Function: rankMedianPosition
*
* Performs median minimisation over one rank.
*
* Parameters:
*
* rankValue - the layer number of this rank
* model - an internal model of the hierarchical layout
* nextRankValue - the layer number whose connected cels are to be laid out
* relative to
*/
mxCoordinateAssignment.prototype.rankMedianPosition = function(rankValue, model, nextRankValue)
{
var rank = model.ranks[rankValue];
// Form an array of the order in which the cell are to be processed
// , the order is given by the weighted sum of the in or out edges,
// depending on whether we're traveling up or down the hierarchy.
var weightedValues = [];
var cellMap = new Object();
for (var i = 0; i < rank.length; i++)
{
var currentCell = rank[i];
weightedValues[i] = new WeightedCellSorter();
weightedValues[i].cell = currentCell;
weightedValues[i].rankIndex = i;
cellMap[currentCell.id] = weightedValues[i];
var nextLayerConnectedCells = null;
if (nextRankValue < rankValue)
{
nextLayerConnectedCells = currentCell
.getPreviousLayerConnectedCells(rankValue);
}
else
{
nextLayerConnectedCells = currentCell
.getNextLayerConnectedCells(rankValue);
}
// Calculate the weighing based on this node type and those this
// node is connected to on the next layer
weightedValues[i].weightedValue = this.calculatedWeightedValue(
currentCell, nextLayerConnectedCells);
}
weightedValues.sort(WeightedCellSorter.prototype.compare);
// Set the new position of each node within the rank using
// its temp variable
for (var i = 0; i < weightedValues.length; i++)
{
var numConnectionsNextLevel = 0;
var cell = weightedValues[i].cell;
var nextLayerConnectedCells = null;
var medianNextLevel = 0;
if (nextRankValue < rankValue)
{
nextLayerConnectedCells = cell.getPreviousLayerConnectedCells(
rankValue).slice();
}
else
{
nextLayerConnectedCells = cell.getNextLayerConnectedCells(
rankValue).slice();
}
if (nextLayerConnectedCells != null)
{
numConnectionsNextLevel = nextLayerConnectedCells.length;
if (numConnectionsNextLevel > 0)
{
medianNextLevel = this.medianXValue(nextLayerConnectedCells,
nextRankValue);
}
else
{
// For case of no connections on the next level set the
// median to be the current position and try to be
// positioned there
medianNextLevel = cell.getGeneralPurposeVariable(rankValue);
}
}
var leftBuffer = 0.0;
var leftLimit = -100000000.0;
for (var j = weightedValues[i].rankIndex - 1; j >= 0;)
{
var weightedValue = cellMap[rank[j].id];
if (weightedValue != null)
{
var leftCell = weightedValue.cell;
if (weightedValue.visited)
{
// The left limit is the right hand limit of that
// cell plus any allowance for unallocated cells
// in-between
leftLimit = leftCell
.getGeneralPurposeVariable(rankValue)
+ leftCell.width
/ 2.0
+ this.intraCellSpacing
+ leftBuffer + cell.width / 2.0;
j = -1;
}
else
{
leftBuffer += leftCell.width + this.intraCellSpacing;
j--;
}
}
}
var rightBuffer = 0.0;
var rightLimit = 100000000.0;
for (var j = weightedValues[i].rankIndex + 1; j < weightedValues.length;)
{
var weightedValue = cellMap[rank[j].id];
if (weightedValue != null)
{
var rightCell = weightedValue.cell;
if (weightedValue.visited)
{
// The left limit is the right hand limit of that
// cell plus any allowance for unallocated cells
// in-between
rightLimit = rightCell
.getGeneralPurposeVariable(rankValue)
- rightCell.width
/ 2.0
- this.intraCellSpacing
- rightBuffer - cell.width / 2.0;
j = weightedValues.length;
}
else
{
rightBuffer += rightCell.width + this.intraCellSpacing;
j++;
}
}
}
if (medianNextLevel >= leftLimit && medianNextLevel <= rightLimit)
{
cell.setGeneralPurposeVariable(rankValue, medianNextLevel);
}
else if (medianNextLevel < leftLimit)
{
// Couldn't place at median value, place as close to that
// value as possible
cell.setGeneralPurposeVariable(rankValue, leftLimit);
this.currentXDelta += leftLimit - medianNextLevel;
}
else if (medianNextLevel > rightLimit)
{
// Couldn't place at median value, place as close to that
// value as possible
cell.setGeneralPurposeVariable(rankValue, rightLimit);
this.currentXDelta += medianNextLevel - rightLimit;
}
weightedValues[i].visited = true;
}
};
/**
* Function: calculatedWeightedValue
*
* Calculates the priority the specified cell has based on the type of its
* cell and the cells it is connected to on the next layer
*
* Parameters:
*
* currentCell - the cell whose weight is to be calculated
* collection - the cells the specified cell is connected to
*/
mxCoordinateAssignment.prototype.calculatedWeightedValue = function(currentCell, collection)
{
var totalWeight = 0;
for (var i = 0; i < collection.length; i++)
{
var cell = collection[i];
if (currentCell.isVertex() && cell.isVertex())
{
totalWeight++;
}
else if (currentCell.isEdge() && cell.isEdge())
{
totalWeight += 8;
}
else
{
totalWeight += 2;
}
}
return totalWeight;
};
/**
* Function: medianXValue
*
* Calculates the median position of the connected cell on the specified
* rank
*
* Parameters:
*
* connectedCells - the cells the candidate connects to on this level
* rankValue - the layer number of this rank
*/
mxCoordinateAssignment.prototype.medianXValue = function(connectedCells, rankValue)
{
if (connectedCells.length == 0)
{
return 0;
}
var medianValues = [];
for (var i = 0; i < connectedCells.length; i++)
{
medianValues[i] = connectedCells[i].getGeneralPurposeVariable(rankValue);
}
medianValues.sort(function(a,b){return a - b;});
if (connectedCells.length % 2 == 1)
{
// For odd numbers of adjacent vertices return the median
return medianValues[Math.floor(connectedCells.length / 2)];
}
else
{
var medianPoint = connectedCells.length / 2;
var leftMedian = medianValues[medianPoint - 1];
var rightMedian = medianValues[medianPoint];
return ((leftMedian + rightMedian) / 2);
}
};
/**
* Function: initialCoords
*
* Sets up the layout in an initial positioning. The ranks are all centered
* as much as possible along the middle vertex in each rank. The other cells
* are then placed as close as possible on either side.
*
* Parameters:
*
* facade - the facade describing the input graph
* model - an internal model of the hierarchical layout
*/
mxCoordinateAssignment.prototype.initialCoords = function(facade, model)
{
this.calculateWidestRank(facade, model);
// Sweep up and down from the widest rank
for (var i = this.widestRank; i >= 0; i--)
{
if (i < model.maxRank)
{
this.rankCoordinates(i, facade, model);
}
}
for (var i = this.widestRank+1; i <= model.maxRank; i++)
{
if (i > 0)
{
this.rankCoordinates(i, facade, model);
}
}
};
/**
* Function: rankCoordinates
*
* Sets up the layout in an initial positioning. All the first cells in each
* rank are moved to the left and the rest of the rank inserted as close
* together as their size and buffering permits. This method works on just
* the specified rank.
*
* Parameters:
*
* rankValue - the current rank being processed
* graph - the facade describing the input graph
* model - an internal model of the hierarchical layout
*/
mxCoordinateAssignment.prototype.rankCoordinates = function(rankValue, graph, model)
{
var rank = model.ranks[rankValue];
var maxY = 0.0;
var localX = this.initialX + (this.widestRankValue - this.rankWidths[rankValue])
/ 2;
// Store whether or not any of the cells' bounds were unavailable so
// to only issue the warning once for all cells
var boundsWarning = false;
for (var i = 0; i < rank.length; i++)
{
var node = rank[i];
if (node.isVertex())
{
var bounds = this.layout.getVertexBounds(node.cell);
if (bounds != null)
{
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_SOUTH)
{
node.width = bounds.width;
node.height = bounds.height;
}
else
{
node.width = bounds.height;
node.height = bounds.width;
}
}
else
{
boundsWarning = true;
}
maxY = Math.max(maxY, node.height);
}
else if (node.isEdge())
{
// The width is the number of additional parallel edges
// time the parallel edge spacing
var numEdges = 1;
if (node.edges != null)
{
numEdges = node.edges.length;
}
else
{
mxLog.warn('edge.edges is null');
}
node.width = (numEdges - 1) * this.parallelEdgeSpacing;
}
// Set the initial x-value as being the best result so far
localX += node.width / 2.0;
node.setX(rankValue, localX);
node.setGeneralPurposeVariable(rankValue, localX);
localX += node.width / 2.0;
localX += this.intraCellSpacing;
}
if (boundsWarning == true)
{
mxLog.warn('At least one cell has no bounds');
}
};
/**
* Function: calculateWidestRank
*
* Calculates the width rank in the hierarchy. Also set the y value of each
* rank whilst performing the calculation
*
* Parameters:
*
* graph - the facade describing the input graph
* model - an internal model of the hierarchical layout
*/
mxCoordinateAssignment.prototype.calculateWidestRank = function(graph, model)
{
// Starting y co-ordinate
var y = -this.interRankCellSpacing;
// Track the widest cell on the last rank since the y
// difference depends on it
var lastRankMaxCellHeight = 0.0;
this.rankWidths = [];
this.rankY = [];
for (var rankValue = model.maxRank; rankValue >= 0; rankValue--)
{
// Keep track of the widest cell on this rank
var maxCellHeight = 0.0;
var rank = model.ranks[rankValue];
var localX = this.initialX;
// Store whether or not any of the cells' bounds were unavailable so
// to only issue the warning once for all cells
var boundsWarning = false;
for (var i = 0; i < rank.length; i++)
{
var node = rank[i];
if (node.isVertex())
{
var bounds = this.layout.getVertexBounds(node.cell);
if (bounds != null)
{
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_SOUTH)
{
node.width = bounds.width;
node.height = bounds.height;
}
else
{
node.width = bounds.height;
node.height = bounds.width;
}
}
else
{
boundsWarning = true;
}
maxCellHeight = Math.max(maxCellHeight, node.height);
}
else if (node.isEdge())
{
// The width is the number of additional parallel edges
// time the parallel edge spacing
var numEdges = 1;
if (node.edges != null)
{
numEdges = node.edges.length;
}
else
{
mxLog.warn('edge.edges is null');
}
node.width = (numEdges - 1) * this.parallelEdgeSpacing;
}
// Set the initial x-value as being the best result so far
localX += node.width / 2.0;
node.setX(rankValue, localX);
node.setGeneralPurposeVariable(rankValue, localX);
localX += node.width / 2.0;
localX += this.intraCellSpacing;
if (localX > this.widestRankValue)
{
this.widestRankValue = localX;
this.widestRank = rankValue;
}
this.rankWidths[rankValue] = localX;
}
if (boundsWarning == true)
{
mxLog.warn('At least one cell has no bounds');
}
this.rankY[rankValue] = y;
var distanceToNextRank = maxCellHeight / 2.0
+ lastRankMaxCellHeight / 2.0 + this.interRankCellSpacing;
lastRankMaxCellHeight = maxCellHeight;
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_WEST)
{
y += distanceToNextRank;
}
else
{
y -= distanceToNextRank;
}
for (var i = 0; i < rank.length; i++)
{
var cell = rank[i];
cell.setY(rankValue, y);
}
}
};
/**
* Function: minPath
*
* Straightens out chains of virtual nodes where possibleacade to those stored after this layout
* processing step has completed.
*
* Parameters:
*
* graph - the facade describing the input graph
* model - an internal model of the hierarchical layout
*/
mxCoordinateAssignment.prototype.minPath = function(graph, model)
{
// Work down and up each edge with at least 2 control points
// trying to straighten each one out. If the same number of
// straight segments are formed in both directions, the
// preferred direction used is the one where the final
// control points have the least offset from the connectable
// region of the terminating vertices
var edges = model.edgeMapper.getValues();
for (var j = 0; j < edges.length; j++)
{
var cell = edges[j];
if (cell.maxRank - cell.minRank - 1 < 1)
{
continue;
}
// At least two virtual nodes in the edge
// Check first whether the edge is already straight
var referenceX = cell
.getGeneralPurposeVariable(cell.minRank + 1);
var edgeStraight = true;
var refSegCount = 0;
for (var i = cell.minRank + 2; i < cell.maxRank; i++)
{
var x = cell.getGeneralPurposeVariable(i);
if (referenceX != x)
{
edgeStraight = false;
referenceX = x;
}
else
{
refSegCount++;
}
}
if (!edgeStraight)
{
var upSegCount = 0;
var downSegCount = 0;
var upXPositions = [];
var downXPositions = [];
var currentX = cell.getGeneralPurposeVariable(cell.minRank + 1);
for (var i = cell.minRank + 1; i < cell.maxRank - 1; i++)
{
// Attempt to straight out the control point on the
// next segment up with the current control point.
var nextX = cell.getX(i + 1);
if (currentX == nextX)
{
upXPositions[i - cell.minRank - 1] = currentX;
upSegCount++;
}
else if (this.repositionValid(model, cell, i + 1, currentX))
{
upXPositions[i - cell.minRank - 1] = currentX;
upSegCount++;
// Leave currentX at same value
}
else
{
upXPositions[i - cell.minRank - 1] = nextX;
currentX = nextX;
}
}
currentX = cell.getX(i);
for (var i = cell.maxRank - 1; i > cell.minRank + 1; i--)
{
// Attempt to straight out the control point on the
// next segment down with the current control point.
var nextX = cell.getX(i - 1);
if (currentX == nextX)
{
downXPositions[i - cell.minRank - 2] = currentX;
downSegCount++;
}
else if (this.repositionValid(model, cell, i - 1, currentX))
{
downXPositions[i - cell.minRank - 2] = currentX;
downSegCount++;
// Leave currentX at same value
}
else
{
downXPositions[i - cell.minRank - 2] = cell.getX(i-1);
currentX = nextX;
}
}
if (downSegCount > refSegCount || upSegCount > refSegCount)
{
if (downSegCount >= upSegCount)
{
// Apply down calculation values
for (var i = cell.maxRank - 2; i > cell.minRank; i--)
{
cell.setX(i, downXPositions[i - cell.minRank - 1]);
}
}
else if (upSegCount > downSegCount)
{
// Apply up calculation values
for (var i = cell.minRank + 2; i < cell.maxRank; i++)
{
cell.setX(i, upXPositions[i - cell.minRank - 2]);
}
}
else
{
// Neither direction provided a favourable result
// But both calculations are better than the
// existing solution, so apply the one with minimal
// offset to attached vertices at either end.
}
}
}
}
};
/**
* Function: repositionValid
*
* Determines whether or not a node may be moved to the specified x
* position on the specified rank
*
* Parameters:
*
* model - the layout model
* cell - the cell being analysed
* rank - the layer of the cell
* position - the x position being sought
*/
mxCoordinateAssignment.prototype.repositionValid = function(model, cell, rank, position)
{
var rankArray = model.ranks[rank];
var rankIndex = -1;
for (var i = 0; i < rankArray.length; i++)
{
if (cell == rankArray[i])
{
rankIndex = i;
break;
}
}
if (rankIndex < 0)
{
return false;
}
var currentX = cell.getGeneralPurposeVariable(rank);
if (position < currentX)
{
// Trying to move node to the left.
if (rankIndex == 0)
{
// Left-most node, can move anywhere
return true;
}
var leftCell = rankArray[rankIndex - 1];
var leftLimit = leftCell.getGeneralPurposeVariable(rank);
leftLimit = leftLimit + leftCell.width / 2
+ this.intraCellSpacing + cell.width / 2;
if (leftLimit <= position)
{
return true;
}
else
{
return false;
}
}
else if (position > currentX)
{
// Trying to move node to the right.
if (rankIndex == rankArray.length - 1)
{
// Right-most node, can move anywhere
return true;
}
var rightCell = rankArray[rankIndex + 1];
var rightLimit = rightCell.getGeneralPurposeVariable(rank);
rightLimit = rightLimit - rightCell.width / 2
- this.intraCellSpacing - cell.width / 2;
if (rightLimit >= position)
{
return true;
}
else
{
return false;
}
}
return true;
};
/**
* Function: setCellLocations
*
* Sets the cell locations in the facade to those stored after this layout
* processing step has completed.
*
* Parameters:
*
* graph - the input graph
* model - the layout model
*/
mxCoordinateAssignment.prototype.setCellLocations = function(graph, model)
{
this.rankTopY = [];
this.rankBottomY = [];
for (var i = 0; i < model.ranks.length; i++)
{
this.rankTopY[i] = Number.MAX_VALUE;
this.rankBottomY[i] = -Number.MAX_VALUE;
}
var vertices = model.vertexMapper.getValues();
// Process vertices all first, since they define the lower and
// limits of each rank. Between these limits lie the channels
// where the edges can be routed across the graph
for (var i = 0; i < vertices.length; i++)
{
this.setVertexLocation(vertices[i]);
}
// Post process edge styles. Needs the vertex locations set for initial
// values of the top and bottoms of each rank
if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.ORTHOGONAL
|| this.layout.edgeStyle == mxHierarchicalEdgeStyle.POLYLINE
|| this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE)
{
this.localEdgeProcessing(model);
}
var edges = model.edgeMapper.getValues();
for (var i = 0; i < edges.length; i++)
{
this.setEdgePosition(edges[i]);
}
};
/**
* Function: localEdgeProcessing
*
* Separates the x position of edges as they connect to vertices
*
* Parameters:
*
* model - the layout model
*/
mxCoordinateAssignment.prototype.localEdgeProcessing = function(model)
{
// Iterate through each vertex, look at the edges connected in
// both directions.
for (var rankIndex = 0; rankIndex < model.ranks.length; rankIndex++)
{
var rank = model.ranks[rankIndex];
for (var cellIndex = 0; cellIndex < rank.length; cellIndex++)
{
var cell = rank[cellIndex];
if (cell.isVertex())
{
var currentCells = cell.getPreviousLayerConnectedCells(rankIndex);
var currentRank = rankIndex - 1;
// Two loops, last connected cells, and next
for (var k = 0; k < 2; k++)
{
if (currentRank > -1
&& currentRank < model.ranks.length
&& currentCells != null
&& currentCells.length > 0)
{
var sortedCells = [];
for (var j = 0; j < currentCells.length; j++)
{
var sorter = new WeightedCellSorter(
currentCells[j], currentCells[j].getX(currentRank));
sortedCells.push(sorter);
}
sortedCells.sort(WeightedCellSorter.prototype.compare);
var leftLimit = cell.x[0] - cell.width / 2;
var rightLimit = leftLimit + cell.width;
// Connected edge count starts at 1 to allow for buffer
// with edge of vertex
var connectedEdgeCount = 0;
var connectedEdgeGroupCount = 0;
var connectedEdges = [];
// Calculate width requirements for all connected edges
for (var j = 0; j < sortedCells.length; j++)
{
var innerCell = sortedCells[j].cell;
var connections;
if (innerCell.isVertex())
{
// Get the connecting edge
if (k == 0)
{
connections = cell.connectsAsSource;
}
else
{
connections = cell.connectsAsTarget;
}
for (var connIndex = 0; connIndex < connections.length; connIndex++)
{
if (connections[connIndex].source == innerCell
|| connections[connIndex].target == innerCell)
{
connectedEdgeCount += connections[connIndex].edges
.length;
connectedEdgeGroupCount++;
connectedEdges.push(connections[connIndex]);
}
}
}
else
{
connectedEdgeCount += innerCell.edges.length;
connectedEdgeGroupCount++;
connectedEdges.push(innerCell);
}
}
var requiredWidth = (connectedEdgeCount + 1)
* this.prefHozEdgeSep;
// Add a buffer on the edges of the vertex if the edge count allows
if (cell.width > requiredWidth
+ (2 * this.prefHozEdgeSep))
{
leftLimit += this.prefHozEdgeSep;
rightLimit -= this.prefHozEdgeSep;
}
var availableWidth = rightLimit - leftLimit;
var edgeSpacing = availableWidth / connectedEdgeCount;
var currentX = leftLimit + edgeSpacing / 2.0;
var currentYOffset = this.minEdgeJetty - this.prefVertEdgeOff;
var maxYOffset = 0;
for (var j = 0; j < connectedEdges.length; j++)
{
var numActualEdges = connectedEdges[j].edges
.length;
var pos = this.jettyPositions[connectedEdges[j].ids[0]];
if (pos == null)
{
pos = [];
this.jettyPositions[connectedEdges[j].ids[0]] = pos;
}
if (j < connectedEdgeCount / 2)
{
currentYOffset += this.prefVertEdgeOff;
}
else if (j > connectedEdgeCount / 2)
{
currentYOffset -= this.prefVertEdgeOff;
}
// Ignore the case if equals, this means the second of 2
// jettys with the same y (even number of edges)
for (var m = 0; m < numActualEdges; m++)
{
pos[m * 4 + k * 2] = currentX;
currentX += edgeSpacing;
pos[m * 4 + k * 2 + 1] = currentYOffset;
}
maxYOffset = Math.max(maxYOffset,
currentYOffset);
}
}
currentCells = cell.getNextLayerConnectedCells(rankIndex);
currentRank = rankIndex + 1;
}
}
}
}
};
/**
* Function: setEdgePosition
*
* Fixes the control points
*/
mxCoordinateAssignment.prototype.setEdgePosition = function(cell)
{
// For parallel edges we need to seperate out the points a
// little
var offsetX = 0;
// Only set the edge control points once
if (cell.temp[0] != 101207)
{
var maxRank = cell.maxRank;
var minRank = cell.minRank;
if (maxRank == minRank)
{
maxRank = cell.source.maxRank;
minRank = cell.target.minRank;
}
var parallelEdgeCount = 0;
var jettys = this.jettyPositions[cell.ids[0]];
var source = cell.isReversed ? cell.target.cell : cell.source.cell;
var graph = this.layout.graph;
var layoutReversed = this.orientation == mxConstants.DIRECTION_EAST
|| this.orientation == mxConstants.DIRECTION_SOUTH;
for (var i = 0; i < cell.edges.length; i++)
{
var realEdge = cell.edges[i];
var realSource = this.layout.getVisibleTerminal(realEdge, true);
//List oldPoints = graph.getPoints(realEdge);
var newPoints = [];
// Single length reversed edges end up with the jettys in the wrong
// places. Since single length edges only have jettys, not segment
// control points, we just say the edge isn't reversed in this section
var reversed = cell.isReversed;
if (realSource != source)
{
// The real edges include all core model edges and these can go
// in both directions. If the source of the hierarchical model edge
// isn't the source of the specific real edge in this iteration
// treat if as reversed
reversed = !reversed;
}
// First jetty of edge
if (jettys != null)
{
var arrayOffset = reversed ? 2 : 0;
var y = reversed ?
(layoutReversed ? this.rankBottomY[minRank] : this.rankTopY[minRank]) :
(layoutReversed ? this.rankTopY[maxRank] : this.rankBottomY[maxRank]);
var jetty = jettys[parallelEdgeCount * 4 + 1 + arrayOffset];
if (reversed != layoutReversed)
{
jetty = -jetty;
}
y += jetty;
var x = jettys[parallelEdgeCount * 4 + arrayOffset];
var modelSource = graph.model.getTerminal(realEdge, true);
if (this.layout.isPort(modelSource) && graph.model.getParent(modelSource) == realSource)
{
var state = graph.view.getState(modelSource);
if (state != null)
{
x = state.x;
}
else
{
x = realSource.geometry.x + cell.source.width * modelSource.geometry.x;
}
}
if (this.orientation == mxConstants.DIRECTION_NORTH
|| this.orientation == mxConstants.DIRECTION_SOUTH)
{
newPoints.push(new mxPoint(x, y));
if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE)
{
newPoints.push(new mxPoint(x, y + jetty));
}
}
else
{
newPoints.push(new mxPoint(y, x));
if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE)
{
newPoints.push(new mxPoint(y + jetty, x));
}
}
}
// Declare variables to define loop through edge points and
// change direction if edge is reversed
var loopStart = cell.x.length - 1;
var loopLimit = -1;
var loopDelta = -1;
var currentRank = cell.maxRank - 1;
if (reversed)
{
loopStart = 0;
loopLimit = cell.x.length;
loopDelta = 1;
currentRank = cell.minRank + 1;
}
// Reversed edges need the points inserted in
// reverse order
for (var j = loopStart; (cell.maxRank != cell.minRank) && j != loopLimit; j += loopDelta)
{
// The horizontal position in a vertical layout
var positionX = cell.x[j] + offsetX;
// Work out the vertical positions in a vertical layout
// in the edge buffer channels above and below this rank
var topChannelY = (this.rankTopY[currentRank] + this.rankBottomY[currentRank + 1]) / 2.0;
var bottomChannelY = (this.rankTopY[currentRank - 1] + this.rankBottomY[currentRank]) / 2.0;
if (reversed)
{
var tmp = topChannelY;
topChannelY = bottomChannelY;
bottomChannelY = tmp;
}
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_SOUTH)
{
newPoints.push(new mxPoint(positionX, topChannelY));
newPoints.push(new mxPoint(positionX, bottomChannelY));
}
else
{
newPoints.push(new mxPoint(topChannelY, positionX));
newPoints.push(new mxPoint(bottomChannelY, positionX));
}
this.limitX = Math.max(this.limitX, positionX);
currentRank += loopDelta;
}
// Second jetty of edge
if (jettys != null)
{
var arrayOffset = reversed ? 2 : 0;
var rankY = reversed ?
(layoutReversed ? this.rankTopY[maxRank] : this.rankBottomY[maxRank]) :
(layoutReversed ? this.rankBottomY[minRank] : this.rankTopY[minRank]);
var jetty = jettys[parallelEdgeCount * 4 + 3 - arrayOffset];
if (reversed != layoutReversed)
{
jetty = -jetty;
}
var y = rankY - jetty;
var x = jettys[parallelEdgeCount * 4 + 2 - arrayOffset];
var modelTarget = graph.model.getTerminal(realEdge, false);
var realTarget = this.layout.getVisibleTerminal(realEdge, false);
if (this.layout.isPort(modelTarget) && graph.model.getParent(modelTarget) == realTarget)
{
var state = graph.view.getState(modelTarget);
if (state != null)
{
x = state.x;
}
else
{
x = realTarget.geometry.x + cell.target.width * modelTarget.geometry.x;
}
}
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_SOUTH)
{
if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE)
{
newPoints.push(new mxPoint(x, y - jetty));
}
newPoints.push(new mxPoint(x, y));
}
else
{
if (this.layout.edgeStyle == mxHierarchicalEdgeStyle.CURVE)
{
newPoints.push(new mxPoint(y - jetty, x));
}
newPoints.push(new mxPoint(y, x));
}
}
if (cell.isReversed)
{
this.processReversedEdge(cell, realEdge);
}
this.layout.setEdgePoints(realEdge, newPoints);
// Increase offset so next edge is drawn next to
// this one
if (offsetX == 0.0)
{
offsetX = this.parallelEdgeSpacing;
}
else if (offsetX > 0)
{
offsetX = -offsetX;
}
else
{
offsetX = -offsetX + this.parallelEdgeSpacing;
}
parallelEdgeCount++;
}
cell.temp[0] = 101207;
}
};
/**
* Function: setVertexLocation
*
* Fixes the position of the specified vertex.
*
* Parameters:
*
* cell - the vertex to position
*/
mxCoordinateAssignment.prototype.setVertexLocation = function(cell)
{
var realCell = cell.cell;
var positionX = cell.x[0] - cell.width / 2;
var positionY = cell.y[0] - cell.height / 2;
this.rankTopY[cell.minRank] = Math.min(this.rankTopY[cell.minRank], positionY);
this.rankBottomY[cell.minRank] = Math.max(this.rankBottomY[cell.minRank],
positionY + cell.height);
if (this.orientation == mxConstants.DIRECTION_NORTH ||
this.orientation == mxConstants.DIRECTION_SOUTH)
{
this.layout.setVertexLocation(realCell, positionX, positionY);
}
else
{
this.layout.setVertexLocation(realCell, positionY, positionX);
}
this.limitX = Math.max(this.limitX, positionX + cell.width);
};
/**
* Function: processReversedEdge
*
* Hook to add additional processing
*
* Parameters:
*
* edge - the hierarchical model edge
* realEdge - the real edge in the graph
*/
mxCoordinateAssignment.prototype.processReversedEdge = function(graph, model)
{
// hook for subclassers
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlaneOrdering
*
* An implementation of the first stage of the Sugiyama layout. Straightforward
* longest path calculation of layer assignment
*
* Constructor: mxSwimlaneOrdering
*
* Creates a cycle remover for the given internal model.
*/
function mxSwimlaneOrdering(layout)
{
this.layout = layout;
};
/**
* Extends mxHierarchicalLayoutStage.
*/
mxSwimlaneOrdering.prototype = new mxHierarchicalLayoutStage();
mxSwimlaneOrdering.prototype.constructor = mxSwimlaneOrdering;
/**
* Variable: layout
*
* Reference to the enclosing <mxHierarchicalLayout>.
*/
mxSwimlaneOrdering.prototype.layout = null;
/**
* Function: execute
*
* Takes the graph detail and configuration information within the facade
* and creates the resulting laid out graph within that facade for further
* use.
*/
mxSwimlaneOrdering.prototype.execute = function(parent)
{
var model = this.layout.getModel();
var seenNodes = new Object();
var unseenNodes = mxUtils.clone(model.vertexMapper, null, true);
// Perform a dfs through the internal model. If a cycle is found,
// reverse it.
var rootsArray = null;
if (model.roots != null)
{
var modelRoots = model.roots;
rootsArray = [];
for (var i = 0; i < modelRoots.length; i++)
{
var nodeId = mxCellPath.create(modelRoots[i]);
rootsArray[i] = model.vertexMapper.get(modelRoots[i]);
}
}
model.visit(function(parent, node, connectingEdge, layer, seen)
{
// Check if the cell is in it's own ancestor list, if so
// invert the connecting edge and reverse the target/source
// relationship to that edge in the parent and the cell
// Ancestor hashes only line up within a swimlane
var isAncestor = parent != null && parent.swimlaneIndex == node.swimlaneIndex && node.isAncestor(parent);
// If the source->target swimlane indices go from higher to
// lower, the edge is reverse
var reversedOverSwimlane = parent != null && connectingEdge != null &&
parent.swimlaneIndex < node.swimlaneIndex && connectingEdge.source == node;
if (isAncestor)
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsSource);
node.connectsAsSource.push(connectingEdge);
parent.connectsAsTarget.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsTarget);
}
else if (reversedOverSwimlane)
{
connectingEdge.invert();
mxUtils.remove(connectingEdge, parent.connectsAsTarget);
node.connectsAsTarget.push(connectingEdge);
parent.connectsAsSource.push(connectingEdge);
mxUtils.remove(connectingEdge, node.connectsAsSource);
}
var cellId = mxCellPath.create(node.cell);
seenNodes[cellId] = node;
delete unseenNodes[cellId];
}, rootsArray, true, null);
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxHierarchicalLayout
*
* A hierarchical layout algorithm.
*
* Constructor: mxHierarchicalLayout
*
* Constructs a new hierarchical layout algorithm.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* orientation - Optional constant that defines the orientation of this
* layout.
* deterministic - Optional boolean that specifies if this layout should be
* deterministic. Default is true.
*/
function mxHierarchicalLayout(graph, orientation, deterministic)
{
mxGraphLayout.call(this, graph);
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH;
this.deterministic = (deterministic != null) ? deterministic : true;
};
var mxHierarchicalEdgeStyle =
{
ORTHOGONAL: 1,
POLYLINE: 2,
STRAIGHT: 3,
CURVE: 4
};
/**
* Extends mxGraphLayout.
*/
mxHierarchicalLayout.prototype = new mxGraphLayout();
mxHierarchicalLayout.prototype.constructor = mxHierarchicalLayout;
/**
* Variable: roots
*
* Holds the array of <mxCell> that this layout contains.
*/
mxHierarchicalLayout.prototype.roots = null;
/**
* Variable: resizeParent
*
* Specifies if the parent should be resized after the layout so that it
* contains all the child cells. Default is false. See also <parentBorder>.
*/
mxHierarchicalLayout.prototype.resizeParent = false;
/**
* Variable: maintainParentLocation
*
* Specifies if the parent location should be maintained, so that the
* top, left corner stays the same before and after execution of
* the layout. Default is false for backwards compatibility.
*/
mxHierarchicalLayout.prototype.maintainParentLocation = false;
/**
* Variable: moveParent
*
* Specifies if the parent should be moved if <resizeParent> is enabled.
* Default is false.
*/
mxHierarchicalLayout.prototype.moveParent = false;
/**
* Variable: parentBorder
*
* The border to be added around the children if the parent is to be
* resized using <resizeParent>. Default is 0.
*/
mxHierarchicalLayout.prototype.parentBorder = 0;
/**
* Variable: intraCellSpacing
*
* The spacing buffer added between cells on the same layer. Default is 30.
*/
mxHierarchicalLayout.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The spacing buffer added between cell on adjacent layers. Default is 100.
*/
mxHierarchicalLayout.prototype.interRankCellSpacing = 100;
/**
* Variable: interHierarchySpacing
*
* The spacing buffer between unconnected hierarchies. Default is 60.
*/
mxHierarchicalLayout.prototype.interHierarchySpacing = 60;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges.
* Default is 10.
*/
mxHierarchicalLayout.prototype.parallelEdgeSpacing = 10;
/**
* Variable: orientation
*
* The position of the root node(s) relative to the laid out graph in.
* Default is <mxConstants.DIRECTION_NORTH>.
*/
mxHierarchicalLayout.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxHierarchicalLayout.prototype.fineTuning = true;
/**
*
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells. Default is true.
*/
mxHierarchicalLayout.prototype.tightenToSource = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxHierarchicalLayout.prototype.disableEdgeStyle = true;
/**
* Variable: traverseAncestors
*
* Whether or not to drill into child cells and layout in reverse
* group order. This also cause the layout to navigate edges whose
* terminal vertices have different parents but are in the same
* ancestry chain. Default is true.
*/
mxHierarchicalLayout.prototype.traverseAncestors = true;
/**
* Variable: model
*
* The internal <mxGraphHierarchyModel> formed of the layout.
*/
mxHierarchicalLayout.prototype.model = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgeSourceTermCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesTargetTermCache = null;
/**
* Variable: edgeStyle
*
* The style to apply between cell layers to edge segments.
* Default is <mxHierarchicalEdgeStyle.POLYLINE>.
*/
mxHierarchicalLayout.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE;
/**
* Function: getModel
*
* Returns the internal <mxGraphHierarchyModel> for this layout algorithm.
*/
mxHierarchicalLayout.prototype.getModel = function()
{
return this.model;
};
/**
* Function: execute
*
* Executes the layout for the children of the specified parent.
*
* Parameters:
*
* parent - Parent <mxCell> that contains the children to be laid out.
* roots - Optional starting roots of the layout.
*/
mxHierarchicalLayout.prototype.execute = function(parent, roots)
{
this.parent = parent;
var model = this.graph.model;
this.edgesCache = new mxDictionary();
this.edgeSourceTermCache = new mxDictionary();
this.edgesTargetTermCache = new mxDictionary();
if (roots != null && !(roots instanceof Array))
{
roots = [roots];
}
// If the roots are set and the parent is set, only
// use the roots that are some dependent of the that
// parent.
// If just the root are set, use them as-is
// If just the parent is set use it's immediate
// children as the initial set
if (roots == null && parent == null)
{
// TODO indicate the problem
return;
}
// Maintaining parent location
this.parentX = null;
this.parentY = null;
if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
this.parentX = geo.x;
this.parentY = geo.y;
}
}
if (roots != null)
{
var rootsCopy = [];
for (var i = 0; i < roots.length; i++)
{
var ancestor = parent != null ? model.isAncestor(parent, roots[i]) : true;
if (ancestor && model.isVertex(roots[i]))
{
rootsCopy.push(roots[i]);
}
}
this.roots = rootsCopy;
}
model.beginUpdate();
try
{
this.run(parent);
if (this.resizeParent && !this.graph.isCellCollapsed(parent))
{
this.graph.updateGroupBounds([parent], this.parentBorder, this.moveParent);
}
// Maintaining parent location
if (this.parentX != null && this.parentY != null)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
geo = geo.clone();
geo.x = this.parentX;
geo.y = this.parentY;
model.setGeometry(parent, geo);
}
}
}
finally
{
model.endUpdate();
}
};
/**
* Function: findRoots
*
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* Parameters:
*
* parent - <mxCell> whose children should be checked.
* vertices - array of vertices to limit search to
*/
mxHierarchicalLayout.prototype.findRoots = function(parent, vertices)
{
var roots = [];
if (parent != null && vertices != null)
{
var model = this.graph.model;
var best = null;
var maxDiff = -100000;
for (var i in vertices)
{
var cell = vertices[i];
if (model.isVertex(cell) && this.graph.isCellVisible(cell))
{
var conns = this.getEdges(cell);
var fanOut = 0;
var fanIn = 0;
for (var k = 0; k < conns.length; k++)
{
var src = this.getVisibleTerminal(conns[k], true);
if (src == cell)
{
fanOut++;
}
else
{
fanIn++;
}
}
if (fanIn == 0 && fanOut > 0)
{
roots.push(cell);
}
var diff = fanOut - fanIn;
if (diff > maxDiff)
{
maxDiff = diff;
best = cell;
}
}
}
if (roots.length == 0 && best != null)
{
roots.push(best);
}
}
return roots;
};
/**
* Function: getEdges
*
* Returns the connected edges for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
*/
mxHierarchicalLayout.prototype.getEdges = function(cell)
{
var cachedEdges = this.edgesCache.get(cell);
if (cachedEdges != null)
{
return cachedEdges;
}
var model = this.graph.model;
var edges = [];
var isCollapsed = this.graph.isCellCollapsed(cell);
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
if (this.isPort(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
else if (isCollapsed || !this.graph.isCellVisible(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
}
edges = edges.concat(model.getEdges(cell, true, true));
var result = [];
for (var i = 0; i < edges.length; i++)
{
var source = this.getVisibleTerminal(edges[i], true);
var target = this.getVisibleTerminal(edges[i], false);
if ((source == target) ||
((source != target) &&
((target == cell && (this.parent == null || this.isAncestor(this.parent, source, this.traverseAncestors))) ||
(source == cell && (this.parent == null || this.isAncestor(this.parent, target, this.traverseAncestors))))))
{
result.push(edges[i]);
}
}
this.edgesCache.put(cell, result);
return result;
};
/**
* Function: getVisibleTerminal
*
* Helper function to return visible terminal for edge allowing for ports
*
* Parameters:
*
* edge - <mxCell> whose edges should be returned.
* source - Boolean that specifies whether the source or target terminal is to be returned
*/
mxHierarchicalLayout.prototype.getVisibleTerminal = function(edge, source)
{
var terminalCache = this.edgesTargetTermCache;
if (source)
{
terminalCache = this.edgeSourceTermCache;
}
var term = terminalCache.get(edge);
if (term != null)
{
return term;
}
var state = this.graph.view.getState(edge);
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
if (terminal == null)
{
terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
}
if (terminal != null)
{
if (this.isPort(terminal))
{
terminal = this.graph.model.getParent(terminal);
}
terminalCache.put(edge, terminal);
}
return terminal;
};
/**
* Function: run
*
* The API method used to exercise the layout upon the graph description
* and produce a separate description of the vertex position and edge
* routing changes made. It runs each stage of the layout that has been
* created.
*/
mxHierarchicalLayout.prototype.run = function(parent)
{
// Separate out unconnected hierarchies
var hierarchyVertices = [];
var allVertexSet = [];
if (this.roots == null && parent != null)
{
var filledVertexSet = Object();
this.filterDescendants(parent, filledVertexSet);
this.roots = [];
var filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
while (!filledVertexSetEmpty)
{
var candidateRoots = this.findRoots(parent, filledVertexSet);
// If the candidate root is an unconnected group cell, remove it from
// the layout. We may need a custom set that holds such groups and forces
// them to be processed for resizing and/or moving.
for (var i = 0; i < candidateRoots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, filledVertexSet);
}
for (var i = 0; i < candidateRoots.length; i++)
{
this.roots.push(candidateRoots[i]);
}
filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
}
}
else
{
// Find vertex set as directed traversal from roots
for (var i = 0; i < this.roots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(this.roots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, null);
}
}
// Iterate through the result removing parents who have children in this layout
// Perform a layout for each seperate hierarchy
// Track initial coordinate x-positioning
var initialX = 0;
for (var i = 0; i < hierarchyVertices.length; i++)
{
var vertexSet = hierarchyVertices[i];
var tmp = [];
for (var key in vertexSet)
{
tmp.push(vertexSet[key]);
}
this.model = new mxGraphHierarchyModel(this, tmp, this.roots,
parent, this.tightenToSource);
this.cycleStage(parent);
this.layeringStage();
this.crossingStage(parent);
initialX = this.placementStage(initialX, parent);
}
};
/**
* Function: filterDescendants
*
* Creates an array of descendant cells
*/
mxHierarchicalLayout.prototype.filterDescendants = function(cell, result)
{
var model = this.graph.model;
if (model.isVertex(cell) && cell != this.parent && this.graph.isCellVisible(cell))
{
result[mxObjectIdentity.get(cell)] = cell;
}
if (this.traverseAncestors || cell == this.parent
&& this.graph.isCellVisible(cell))
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
// Ignore ports in the layout vertex list, they are dealt with
// in the traversal mechanisms
if (!this.isPort(child))
{
this.filterDescendants(child, result);
}
}
}
};
/**
* Function: isPort
*
* Returns true if the given cell is a "port", that is, when connecting to
* it, its parent is the connecting vertex in terms of graph traversal
*
* Parameters:
*
* cell - <mxCell> that represents the port.
*/
mxHierarchicalLayout.prototype.isPort = function(cell)
{
if (cell != null && cell.geometry != null)
{
return cell.geometry.relative;
}
else
{
return false;
}
};
/**
* Function: getEdgesBetween
*
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and ports.
*
* Parameters:
*
* source -
* target -
* directed -
*/
mxHierarchicalLayout.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var edges = this.getEdges(source);
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edges.length; i++)
{
var src = this.getVisibleTerminal(edges[i], true);
var trg = this.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target) || (!directed && src == target && trg == source))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - boolean indicating if edges should only be traversed
* from source to target. Default is true.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* allVertices - Array of cell paths for the visited cells.
*/
mxHierarchicalLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp,
hierarchyVertices, filledVertexSet)
{
if (vertex != null && allVertices != null)
{
// Has this vertex been seen before in any traversal
// And if the filled vertex set is populated, only
// process vertices in that it contains
var vertexID = mxObjectIdentity.get(vertex);
if ((allVertices[vertexID] == null)
&& (filledVertexSet == null ? true : filledVertexSet[vertexID] != null))
{
if (currentComp[vertexID] == null)
{
currentComp[vertexID] = vertex;
}
if (allVertices[vertexID] == null)
{
allVertices[vertexID] = vertex;
}
if (filledVertexSet !== null)
{
delete filledVertexSet[vertexID];
}
var edges = this.getEdges(vertex);
var edgeIsSource = [];
for (var i = 0; i < edges.length; i++)
{
edgeIsSource[i] = (this.getVisibleTerminal(edges[i], true) == vertex);
}
for (var i = 0; i < edges.length; i++)
{
if (!directed || edgeIsSource[i])
{
var next = this.getVisibleTerminal(edges[i], !edgeIsSource[i]);
// Check whether there are more edges incoming from the target vertex than outgoing
// The hierarchical model treats bi-directional parallel edges as being sourced
// from the more "sourced" terminal. If the directions are equal in number, the direction
// is that of the natural direction from the roots of the layout.
// The checks below are slightly more verbose than need be for performance reasons
var netCount = 1;
for (var j = 0; j < edges.length; j++)
{
if (j == i)
{
continue;
}
else
{
var isSource2 = edgeIsSource[j];
var otherTerm = this.getVisibleTerminal(edges[j], !isSource2);
if (otherTerm == next)
{
if (isSource2)
{
netCount++;
}
else
{
netCount--;
}
}
}
}
if (netCount >= 0)
{
currentComp = this.traverse(next, directed, edges[i], allVertices,
currentComp, hierarchyVertices,
filledVertexSet);
}
}
}
}
else
{
if (currentComp[vertexID] == null)
{
// We've seen this vertex before, but not in the current component
// This component and the one it's in need to be merged
for (var i = 0; i < hierarchyVertices.length; i++)
{
var comp = hierarchyVertices[i];
if (comp[vertexID] != null)
{
for (var key in comp)
{
currentComp[key] = comp[key];
}
// Remove the current component from the hierarchy set
hierarchyVertices.splice(i, 1);
return currentComp;
}
}
}
}
}
return currentComp;
};
/**
* Function: cycleStage
*
* Executes the cycle stage using mxMinimumCycleRemover.
*/
mxHierarchicalLayout.prototype.cycleStage = function(parent)
{
var cycleStage = new mxMinimumCycleRemover(this);
cycleStage.execute(parent);
};
/**
* Function: layeringStage
*
* Implements first stage of a Sugiyama layout.
*/
mxHierarchicalLayout.prototype.layeringStage = function()
{
this.model.initialRank();
this.model.fixRanks();
};
/**
* Function: crossingStage
*
* Executes the crossing stage using mxMedianHybridCrossingReduction.
*/
mxHierarchicalLayout.prototype.crossingStage = function(parent)
{
var crossingStage = new mxMedianHybridCrossingReduction(this);
crossingStage.execute(parent);
};
/**
* Function: placementStage
*
* Executes the placement stage using mxCoordinateAssignment.
*/
mxHierarchicalLayout.prototype.placementStage = function(initialX, parent)
{
var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing,
this.interRankCellSpacing, this.orientation, initialX,
this.parallelEdgeSpacing);
placementStage.fineTuning = this.fineTuning;
placementStage.execute(parent);
return placementStage.limitX + this.interHierarchySpacing;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlaneLayout
*
* A hierarchical layout algorithm.
*
* Constructor: mxSwimlaneLayout
*
* Constructs a new hierarchical layout algorithm.
*
* Arguments:
*
* graph - Reference to the enclosing <mxGraph>.
* orientation - Optional constant that defines the orientation of this
* layout.
* deterministic - Optional boolean that specifies if this layout should be
* deterministic. Default is true.
*/
function mxSwimlaneLayout(graph, orientation, deterministic)
{
mxGraphLayout.call(this, graph);
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH;
this.deterministic = (deterministic != null) ? deterministic : true;
};
/**
* Extends mxGraphLayout.
*/
mxSwimlaneLayout.prototype = new mxGraphLayout();
mxSwimlaneLayout.prototype.constructor = mxSwimlaneLayout;
/**
* Variable: roots
*
* Holds the array of <mxCell> that this layout contains.
*/
mxSwimlaneLayout.prototype.roots = null;
/**
* Variable: swimlanes
*
* Holds the array of <mxCell> of the ordered swimlanes to lay out
*/
mxSwimlaneLayout.prototype.swimlanes = null;
/**
* Variable: dummyVertexWidth
*
* The cell width of any dummy vertices inserted
*/
mxSwimlaneLayout.prototype.dummyVertexWidth = 50;
/**
* Variable: resizeParent
*
* Specifies if the parent should be resized after the layout so that it
* contains all the child cells. Default is false. See also <parentBorder>.
*/
mxSwimlaneLayout.prototype.resizeParent = false;
/**
* Variable: maintainParentLocation
*
* Specifies if the parent location should be maintained, so that the
* top, left corner stays the same before and after execution of
* the layout. Default is false for backwards compatibility.
*/
mxSwimlaneLayout.prototype.maintainParentLocation = false;
/**
* Variable: moveParent
*
* Specifies if the parent should be moved if <resizeParent> is enabled.
* Default is false.
*/
mxSwimlaneLayout.prototype.moveParent = false;
/**
* Variable: parentBorder
*
* The border to be added around the children if the parent is to be
* resized using <resizeParent>. Default is 30.
*/
mxSwimlaneLayout.prototype.parentBorder = 30;
/**
* Variable: intraCellSpacing
*
* The spacing buffer added between cells on the same layer. Default is 30.
*/
mxSwimlaneLayout.prototype.intraCellSpacing = 30;
/**
* Variable: interRankCellSpacing
*
* The spacing buffer added between cell on adjacent layers. Default is 100.
*/
mxSwimlaneLayout.prototype.interRankCellSpacing = 100;
/**
* Variable: interHierarchySpacing
*
* The spacing buffer between unconnected hierarchies. Default is 60.
*/
mxSwimlaneLayout.prototype.interHierarchySpacing = 60;
/**
* Variable: parallelEdgeSpacing
*
* The distance between each parallel edge on each ranks for long edges.
* Default is 10.
*/
mxSwimlaneLayout.prototype.parallelEdgeSpacing = 10;
/**
* Variable: orientation
*
* The position of the root node(s) relative to the laid out graph in.
* Default is <mxConstants.DIRECTION_NORTH>.
*/
mxSwimlaneLayout.prototype.orientation = mxConstants.DIRECTION_NORTH;
/**
* Variable: fineTuning
*
* Whether or not to perform local optimisations and iterate multiple times
* through the algorithm. Default is true.
*/
mxSwimlaneLayout.prototype.fineTuning = true;
/**
* Variable: tightenToSource
*
* Whether or not to tighten the assigned ranks of vertices up towards
* the source cells. Default is true.
*/
mxSwimlaneLayout.prototype.tightenToSource = true;
/**
* Variable: disableEdgeStyle
*
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are
* modified by the result. Default is true.
*/
mxSwimlaneLayout.prototype.disableEdgeStyle = true;
/**
* Variable: traverseAncestors
*
* Whether or not to drill into child cells and layout in reverse
* group order. This also cause the layout to navigate edges whose
* terminal vertices have different parents but are in the same
* ancestry chain. Default is true.
*/
mxSwimlaneLayout.prototype.traverseAncestors = true;
/**
* Variable: model
*
* The internal <mxSwimlaneModel> formed of the layout.
*/
mxSwimlaneLayout.prototype.model = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxSwimlaneLayout.prototype.edgesCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgeSourceTermCache = null;
/**
* Variable: edgesSet
*
* A cache of edges whose source terminal is the key
*/
mxHierarchicalLayout.prototype.edgesTargetTermCache = null;
/**
* Variable: edgeStyle
*
* The style to apply between cell layers to edge segments.
* Default is <mxHierarchicalEdgeStyle.POLYLINE>.
*/
mxHierarchicalLayout.prototype.edgeStyle = mxHierarchicalEdgeStyle.POLYLINE;
/**
* Function: getModel
*
* Returns the internal <mxSwimlaneModel> for this layout algorithm.
*/
mxSwimlaneLayout.prototype.getModel = function()
{
return this.model;
};
/**
* Function: execute
*
* Executes the layout for the children of the specified parent.
*
* Parameters:
*
* parent - Parent <mxCell> that contains the children to be laid out.
* swimlanes - Ordered array of swimlanes to be laid out
*/
mxSwimlaneLayout.prototype.execute = function(parent, swimlanes)
{
this.parent = parent;
var model = this.graph.model;
this.edgesCache = new mxDictionary();
this.edgeSourceTermCache = new mxDictionary();
this.edgesTargetTermCache = new mxDictionary();
// If the roots are set and the parent is set, only
// use the roots that are some dependent of the that
// parent.
// If just the root are set, use them as-is
// If just the parent is set use it's immediate
// children as the initial set
if (swimlanes == null || swimlanes.length < 1)
{
// TODO indicate the problem
return;
}
if (parent == null)
{
parent = model.getParent(swimlanes[0]);
}
// Maintaining parent location
this.parentX = null;
this.parentY = null;
if (parent != this.root && model.isVertex(parent) != null && this.maintainParentLocation)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
this.parentX = geo.x;
this.parentY = geo.y;
}
}
this.swimlanes = swimlanes;
var dummyVertices = [];
// Check the swimlanes all have vertices
// in them
for (var i = 0; i < swimlanes.length; i++)
{
var children = this.graph.getChildCells(swimlanes[i]);
if (children == null || children.length == 0)
{
var vertex = this.graph.insertVertex(swimlanes[i], null, null, 0, 0, this.dummyVertexWidth, 0);
dummyVertices.push(vertex);
}
}
model.beginUpdate();
try
{
this.run(parent);
if (this.resizeParent && !this.graph.isCellCollapsed(parent))
{
this.graph.updateGroupBounds([parent], this.parentBorder, this.moveParent);
}
// Maintaining parent location
if (this.parentX != null && this.parentY != null)
{
var geo = this.graph.getCellGeometry(parent);
if (geo != null)
{
geo = geo.clone();
geo.x = this.parentX;
geo.y = this.parentY;
model.setGeometry(parent, geo);
}
}
this.graph.removeCells(dummyVertices);
}
finally
{
model.endUpdate();
}
};
/**
* Function: updateGroupBounds
*
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
*/
mxSwimlaneLayout.prototype.updateGroupBounds = function()
{
// Get all vertices and edge in the layout
var cells = [];
var model = this.model;
for (var key in model.edgeMapper)
{
var edge = model.edgeMapper[key];
for (var i = 0; i < edge.edges.length; i++)
{
cells.push(edge.edges[i]);
}
}
var layoutBounds = this.graph.getBoundingBoxFromGeometry(cells, true);
var childBounds = [];
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var bounds = this.graph.getBoundingBoxFromGeometry(children);
childBounds[i] = bounds;
var childrenY = bounds.y + geo.y - size.height - this.parentBorder;
var maxChildrenY = bounds.y + geo.y + bounds.height;
if (layoutBounds == null)
{
layoutBounds = new mxRectangle(0, childrenY, 0, maxChildrenY - childrenY);
}
else
{
layoutBounds.y = Math.min(layoutBounds.y, childrenY);
var maxY = Math.max(layoutBounds.y + layoutBounds.height, maxChildrenY);
layoutBounds.height = maxY - layoutBounds.y;
}
}
}
for (var i = 0; i < this.swimlanes.length; i++)
{
var lane = this.swimlanes[i];
var geo = this.graph.getCellGeometry(lane);
if (geo != null)
{
var children = this.graph.getChildCells(lane);
var size = (this.graph.isSwimlane(lane)) ?
this.graph.getStartSize(lane) : new mxRectangle();
var newGeo = geo.clone();
var leftGroupBorder = (i == 0) ? this.parentBorder : this.interRankCellSpacing/2;
var w = size.width + leftGroupBorder;
var x = childBounds[i].x - w;
var y = layoutBounds.y - this.parentBorder;
newGeo.x += x;
newGeo.y = y;
newGeo.width = childBounds[i].width + w + this.interRankCellSpacing/2;
newGeo.height = layoutBounds.height + size.height + 2 * this.parentBorder;
this.graph.model.setGeometry(lane, newGeo);
this.graph.moveCells(children, -x, geo.y - y);
}
}
};
/**
* Function: findRoots
*
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* Parameters:
*
* parent - <mxCell> whose children should be checked.
* vertices - array of vertices to limit search to
*/
mxSwimlaneLayout.prototype.findRoots = function(parent, vertices)
{
var roots = [];
if (parent != null && vertices != null)
{
var model = this.graph.model;
var best = null;
var maxDiff = -100000;
for (var i in vertices)
{
var cell = vertices[i];
if (cell != null && model.isVertex(cell) && this.graph.isCellVisible(cell) && model.isAncestor(parent, cell))
{
var conns = this.getEdges(cell);
var fanOut = 0;
var fanIn = 0;
for (var k = 0; k < conns.length; k++)
{
var src = this.getVisibleTerminal(conns[k], true);
if (src == cell)
{
// Only count connection within this swimlane
var other = this.getVisibleTerminal(conns[k], false);
if (model.isAncestor(parent, other))
{
fanOut++;
}
}
else if (model.isAncestor(parent, src))
{
fanIn++;
}
}
if (fanIn == 0 && fanOut > 0)
{
roots.push(cell);
}
var diff = fanOut - fanIn;
if (diff > maxDiff)
{
maxDiff = diff;
best = cell;
}
}
}
if (roots.length == 0 && best != null)
{
roots.push(best);
}
}
return roots;
};
/**
* Function: getEdges
*
* Returns the connected edges for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
*/
mxSwimlaneLayout.prototype.getEdges = function(cell)
{
var cachedEdges = this.edgesCache.get(cell);
if (cachedEdges != null)
{
return cachedEdges;
}
var model = this.graph.model;
var edges = [];
var isCollapsed = this.graph.isCellCollapsed(cell);
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
if (this.isPort(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
else if (isCollapsed || !this.graph.isCellVisible(child))
{
edges = edges.concat(model.getEdges(child, true, true));
}
}
edges = edges.concat(model.getEdges(cell, true, true));
var result = [];
for (var i = 0; i < edges.length; i++)
{
var source = this.getVisibleTerminal(edges[i], true);
var target = this.getVisibleTerminal(edges[i], false);
if ((source == target) || ((source != target) && ((target == cell && (this.parent == null || this.graph.isValidAncestor(source, this.parent, this.traverseAncestors))) ||
(source == cell && (this.parent == null ||
this.graph.isValidAncestor(target, this.parent, this.traverseAncestors))))))
{
result.push(edges[i]);
}
}
this.edgesCache.put(cell, result);
return result;
};
/**
* Function: getVisibleTerminal
*
* Helper function to return visible terminal for edge allowing for ports
*
* Parameters:
*
* edge - <mxCell> whose edges should be returned.
* source - Boolean that specifies whether the source or target terminal is to be returned
*/
mxSwimlaneLayout.prototype.getVisibleTerminal = function(edge, source)
{
var terminalCache = this.edgesTargetTermCache;
if (source)
{
terminalCache = this.edgeSourceTermCache;
}
var term = terminalCache.get(edge);
if (term != null)
{
return term;
}
var state = this.graph.view.getState(edge);
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
if (terminal == null)
{
terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source);
}
if (terminal != null)
{
if (this.isPort(terminal))
{
terminal = this.graph.model.getParent(terminal);
}
terminalCache.put(edge, terminal);
}
return terminal;
};
/**
* Function: run
*
* The API method used to exercise the layout upon the graph description
* and produce a separate description of the vertex position and edge
* routing changes made. It runs each stage of the layout that has been
* created.
*/
mxSwimlaneLayout.prototype.run = function(parent)
{
// Separate out unconnected hierarchies
var hierarchyVertices = [];
var allVertexSet = Object();
if (this.swimlanes != null && this.swimlanes.length > 0 && parent != null)
{
var filledVertexSet = Object();
for (var i = 0; i < this.swimlanes.length; i++)
{
this.filterDescendants(this.swimlanes[i], filledVertexSet);
}
this.roots = [];
var filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
// Only test for candidates in each swimlane in order
var laneCounter = 0;
while (!filledVertexSetEmpty && laneCounter < this.swimlanes.length)
{
var candidateRoots = this.findRoots(this.swimlanes[laneCounter], filledVertexSet);
if (candidateRoots.length == 0)
{
laneCounter++;
continue;
}
// If the candidate root is an unconnected group cell, remove it from
// the layout. We may need a custom set that holds such groups and forces
// them to be processed for resizing and/or moving.
for (var i = 0; i < candidateRoots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, filledVertexSet, laneCounter);
}
for (var i = 0; i < candidateRoots.length; i++)
{
this.roots.push(candidateRoots[i]);
}
filledVertexSetEmpty = true;
// Poor man's isSetEmpty
for (var key in filledVertexSet)
{
if (filledVertexSet[key] != null)
{
filledVertexSetEmpty = false;
break;
}
}
}
}
else
{
// Find vertex set as directed traversal from roots
for (var i = 0; i < this.roots.length; i++)
{
var vertexSet = Object();
hierarchyVertices.push(vertexSet);
this.traverse(this.roots[i], true, null, allVertexSet, vertexSet,
hierarchyVertices, null);
}
}
var tmp = [];
for (var key in allVertexSet)
{
tmp.push(allVertexSet[key]);
}
this.model = new mxSwimlaneModel(this, tmp, this.roots,
parent, this.tightenToSource);
this.cycleStage(parent);
this.layeringStage();
this.crossingStage(parent);
this.placementStage(0, parent);
};
/**
* Function: filterDescendants
*
* Creates an array of descendant cells
*/
mxSwimlaneLayout.prototype.filterDescendants = function(cell, result)
{
var model = this.graph.model;
if (model.isVertex(cell) && cell != this.parent && model.getParent(cell) != this.parent && this.graph.isCellVisible(cell))
{
result[mxObjectIdentity.get(cell)] = cell;
}
if (this.traverseAncestors || cell == this.parent
&& this.graph.isCellVisible(cell))
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
// Ignore ports in the layout vertex list, they are dealt with
// in the traversal mechanisms
if (!this.isPort(child))
{
this.filterDescendants(child, result);
}
}
}
};
/**
* Function: isPort
*
* Returns true if the given cell is a "port", that is, when connecting to
* it, its parent is the connecting vertex in terms of graph traversal
*
* Parameters:
*
* cell - <mxCell> that represents the port.
*/
mxSwimlaneLayout.prototype.isPort = function(cell)
{
if (cell.geometry.relative)
{
return true;
}
return false;
};
/**
* Function: getEdgesBetween
*
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and ports.
*
* Parameters:
*
* source -
* target -
* directed -
*/
mxSwimlaneLayout.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var edges = this.getEdges(source);
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edges.length; i++)
{
var src = this.getVisibleTerminal(edges[i], true);
var trg = this.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target) || (!directed && src == target && trg == source))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - boolean indicating if edges should only be traversed
* from source to target. Default is true.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* allVertices - Array of cell paths for the visited cells.
* swimlaneIndex - the laid out order index of the swimlane vertex is contained in
*/
mxSwimlaneLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp,
hierarchyVertices, filledVertexSet, swimlaneIndex)
{
if (vertex != null && allVertices != null)
{
// Has this vertex been seen before in any traversal
// And if the filled vertex set is populated, only
// process vertices in that it contains
var vertexID = mxObjectIdentity.get(vertex);
if ((allVertices[vertexID] == null)
&& (filledVertexSet == null ? true : filledVertexSet[vertexID] != null))
{
if (currentComp[vertexID] == null)
{
currentComp[vertexID] = vertex;
}
if (allVertices[vertexID] == null)
{
allVertices[vertexID] = vertex;
}
if (filledVertexSet !== null)
{
delete filledVertexSet[vertexID];
}
var edges = this.getEdges(vertex);
var model = this.graph.model;
for (var i = 0; i < edges.length; i++)
{
var otherVertex = this.getVisibleTerminal(edges[i], true);
var isSource = otherVertex == vertex;
if (isSource)
{
otherVertex = this.getVisibleTerminal(edges[i], false);
}
var otherIndex = 0;
// Get the swimlane index of the other terminal
for (otherIndex = 0; otherIndex < this.swimlanes.length; otherIndex++)
{
if (model.isAncestor(this.swimlanes[otherIndex], otherVertex))
{
break;
}
}
if (otherIndex >= this.swimlanes.length)
{
continue;
}
// Traverse if the other vertex is within the same swimlane as
// as the current vertex, or if the swimlane index of the other
// vertex is greater than that of this vertex
if ((otherIndex > swimlaneIndex) ||
((!directed || isSource) && otherIndex == swimlaneIndex))
{
currentComp = this.traverse(otherVertex, directed, edges[i], allVertices,
currentComp, hierarchyVertices,
filledVertexSet, otherIndex);
}
}
}
else
{
if (currentComp[vertexID] == null)
{
// We've seen this vertex before, but not in the current component
// This component and the one it's in need to be merged
for (var i = 0; i < hierarchyVertices.length; i++)
{
var comp = hierarchyVertices[i];
if (comp[vertexID] != null)
{
for (var key in comp)
{
currentComp[key] = comp[key];
}
// Remove the current component from the hierarchy set
hierarchyVertices.splice(i, 1);
return currentComp;
}
}
}
}
}
return currentComp;
};
/**
* Function: cycleStage
*
* Executes the cycle stage using mxMinimumCycleRemover.
*/
mxSwimlaneLayout.prototype.cycleStage = function(parent)
{
var cycleStage = new mxSwimlaneOrdering(this);
cycleStage.execute(parent);
};
/**
* Function: layeringStage
*
* Implements first stage of a Sugiyama layout.
*/
mxSwimlaneLayout.prototype.layeringStage = function()
{
this.model.initialRank();
this.model.fixRanks();
};
/**
* Function: crossingStage
*
* Executes the crossing stage using mxMedianHybridCrossingReduction.
*/
mxSwimlaneLayout.prototype.crossingStage = function(parent)
{
var crossingStage = new mxMedianHybridCrossingReduction(this);
crossingStage.execute(parent);
};
/**
* Function: placementStage
*
* Executes the placement stage using mxCoordinateAssignment.
*/
mxSwimlaneLayout.prototype.placementStage = function(initialX, parent)
{
var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing,
this.interRankCellSpacing, this.orientation, initialX,
this.parallelEdgeSpacing);
placementStage.fineTuning = this.fineTuning;
placementStage.execute(parent);
return placementStage.limitX + this.interHierarchySpacing;
};
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxGraphModel
*
* Extends <mxEventSource> to implement a graph model. The graph model acts as
* a wrapper around the cells which are in charge of storing the actual graph
* datastructure. The model acts as a transactional wrapper with event
* notification for all changes, whereas the cells contain the atomic
* operations for updating the actual datastructure.
*
* Layers:
*
* The cell hierarchy in the model must have a top-level root cell which
* contains the layers (typically one default layer), which in turn contain the
* top-level cells of the layers. This means each cell is contained in a layer.
* If no layers are required, then all new cells should be added to the default
* layer.
*
* Layers are useful for hiding and showing groups of cells, or for placing
* groups of cells on top of other cells in the display. To identify a layer,
* the <isLayer> function is used. It returns true if the parent of the given
* cell is the root of the model.
*
* Events:
*
* See events section for more details. There is a new set of events for
* tracking transactional changes as they happen. The events are called
* startEdit for the initial beginUpdate, executed for each executed change
* and endEdit for the terminal endUpdate. The executed event contains a
* property called change which represents the change after execution.
*
* Encoding the model:
*
* To encode a graph model, use the following code:
*
* (code)
* var enc = new mxCodec();
* var node = enc.encode(graph.getModel());
* (end)
*
* This will create an XML node that contains all the model information.
*
* Encoding and decoding changes:
*
* For the encoding of changes, a graph model listener is required that encodes
* each change from the given array of changes.
*
* (code)
* model.addListener(mxEvent.CHANGE, function(sender, evt)
* {
* var changes = evt.getProperty('edit').changes;
* var nodes = [];
* var codec = new mxCodec();
*
* for (var i = 0; i < changes.length; i++)
* {
* nodes.push(codec.encode(changes[i]));
* }
* // do something with the nodes
* });
* (end)
*
* For the decoding and execution of changes, the codec needs a lookup function
* that allows it to resolve cell IDs as follows:
*
* (code)
* var codec = new mxCodec();
* codec.lookup = function(id)
* {
* return model.getCell(id);
* }
* (end)
*
* For each encoded change (represented by a node), the following code can be
* used to carry out the decoding and create a change object.
*
* (code)
* var changes = [];
* var change = codec.decode(node);
* change.model = model;
* change.execute();
* changes.push(change);
* (end)
*
* The changes can then be dispatched using the model as follows.
*
* (code)
* var edit = new mxUndoableEdit(model, false);
* edit.changes = changes;
*
* edit.notify = function()
* {
* edit.source.fireEvent(new mxEventObject(mxEvent.CHANGE,
* 'edit', edit, 'changes', edit.changes));
* edit.source.fireEvent(new mxEventObject(mxEvent.NOTIFY,
* 'edit', edit, 'changes', edit.changes));
* }
*
* model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
* model.fireEvent(new mxEventObject(mxEvent.CHANGE,
* 'edit', edit, 'changes', changes));
* (end)
*
* Event: mxEvent.CHANGE
*
* Fires when an undoable edit is dispatched. The <code>edit</code> property
* contains the <mxUndoableEdit>. The <code>changes</code> property contains
* the array of atomic changes inside the undoable edit. The changes property
* is <strong>deprecated</strong>, please use edit.changes instead.
*
* Example:
*
* For finding newly inserted cells, the following code can be used:
*
* (code)
* graph.model.addListener(mxEvent.CHANGE, function(sender, evt)
* {
* var changes = evt.getProperty('edit').changes;
*
* for (var i = 0; i < changes.length; i++)
* {
* var change = changes[i];
*
* if (change instanceof mxChildChange &&
* change.change.previous == null)
* {
* graph.startEditingAtCell(change.child);
* break;
* }
* }
* });
* (end)
*
*
* Event: mxEvent.NOTIFY
*
* Same as <mxEvent.CHANGE>, this event can be used for classes that need to
* implement a sync mechanism between this model and, say, a remote model. In
* such a setup, only local changes should trigger a notify event and all
* changes should trigger a change event.
*
* Event: mxEvent.EXECUTE
*
* Fires between begin- and endUpdate and after an atomic change was executed
* in the model. The <code>change</code> property contains the atomic change
* that was executed.
*
* Event: mxEvent.EXECUTED
*
* Fires between START_EDIT and END_EDIT after an atomic change was executed.
* The <code>change</code> property contains the change that was executed.
*
* Event: mxEvent.BEGIN_UPDATE
*
* Fires after the <updateLevel> was incremented in <beginUpdate>. This event
* contains no properties.
*
* Event: mxEvent.START_EDIT
*
* Fires after the <updateLevel> was changed from 0 to 1. This event
* contains no properties.
*
* Event: mxEvent.END_UPDATE
*
* Fires after the <updateLevel> was decreased in <endUpdate> but before any
* notification or change dispatching. The <code>edit</code> property contains
* the <currentEdit>.
*
* Event: mxEvent.END_EDIT
*
* Fires after the <updateLevel> was changed from 1 to 0. This event
* contains no properties.
*
* Event: mxEvent.BEFORE_UNDO
*
* Fires before the change is dispatched after the update level has reached 0
* in <endUpdate>. The <code>edit</code> property contains the <curreneEdit>.
*
* Event: mxEvent.UNDO
*
* Fires after the change was dispatched in <endUpdate>. The <code>edit</code>
* property contains the <currentEdit>.
*
* Constructor: mxGraphModel
*
* Constructs a new graph model. If no root is specified then a new root
* <mxCell> with a default layer is created.
*
* Parameters:
*
* root - <mxCell> that represents the root cell.
*/
function mxGraphModel(root)
{
this.currentEdit = this.createUndoableEdit();
if (root != null)
{
this.setRoot(root);
}
else
{
this.clear();
}
};
/**
* Extends mxEventSource.
*/
mxGraphModel.prototype = new mxEventSource();
mxGraphModel.prototype.constructor = mxGraphModel;
/**
* Variable: root
*
* Holds the root cell, which in turn contains the cells that represent the
* layers of the diagram as child cells. That is, the actual elements of the
* diagram are supposed to live in the third generation of cells and below.
*/
mxGraphModel.prototype.root = null;
/**
* Variable: cells
*
* Maps from Ids to cells.
*/
mxGraphModel.prototype.cells = null;
/**
* Variable: maintainEdgeParent
*
* Specifies if edges should automatically be moved into the nearest common
* ancestor of their terminals. Default is true.
*/
mxGraphModel.prototype.maintainEdgeParent = true;
/**
* Variable: ignoreRelativeEdgeParent
*
* Specifies if relative edge parents should be ignored for finding the nearest
* common ancestors of an edge's terminals. Default is true.
*/
mxGraphModel.prototype.ignoreRelativeEdgeParent = true;
/**
* Variable: createIds
*
* Specifies if the model should automatically create Ids for new cells.
* Default is true.
*/
mxGraphModel.prototype.createIds = true;
/**
* Variable: prefix
*
* Defines the prefix of new Ids. Default is an empty string.
*/
mxGraphModel.prototype.prefix = '';
/**
* Variable: postfix
*
* Defines the postfix of new Ids. Default is an empty string.
*/
mxGraphModel.prototype.postfix = '';
/**
* Variable: nextId
*
* Specifies the next Id to be created. Initial value is 0.
*/
mxGraphModel.prototype.nextId = 0;
/**
* Variable: currentEdit
*
* Holds the changes for the current transaction. If the transaction is
* closed then a new object is created for this variable using
* <createUndoableEdit>.
*/
mxGraphModel.prototype.currentEdit = null;
/**
* Variable: updateLevel
*
* Counter for the depth of nested transactions. Each call to <beginUpdate>
* will increment this number and each call to <endUpdate> will decrement
* it. When the counter reaches 0, the transaction is closed and the
* respective events are fired. Initial value is 0.
*/
mxGraphModel.prototype.updateLevel = 0;
/**
* Variable: endingUpdate
*
* True if the program flow is currently inside endUpdate.
*/
mxGraphModel.prototype.endingUpdate = false;
/**
* Function: clear
*
* Sets a new root using <createRoot>.
*/
mxGraphModel.prototype.clear = function()
{
this.setRoot(this.createRoot());
};
/**
* Function: isCreateIds
*
* Returns <createIds>.
*/
mxGraphModel.prototype.isCreateIds = function()
{
return this.createIds;
};
/**
* Function: setCreateIds
*
* Sets <createIds>.
*/
mxGraphModel.prototype.setCreateIds = function(value)
{
this.createIds = value;
};
/**
* Function: createRoot
*
* Creates a new root cell with a default layer (child 0).
*/
mxGraphModel.prototype.createRoot = function()
{
var cell = new mxCell();
cell.insert(new mxCell());
return cell;
};
/**
* Function: getCell
*
* Returns the <mxCell> for the specified Id or null if no cell can be
* found for the given Id.
*
* Parameters:
*
* id - A string representing the Id of the cell.
*/
mxGraphModel.prototype.getCell = function(id)
{
return (this.cells != null) ? this.cells[id] : null;
};
/**
* Function: filterCells
*
* Returns the cells from the given array where the given filter function
* returns true.
*/
mxGraphModel.prototype.filterCells = function(cells, filter)
{
var result = null;
if (cells != null)
{
result = [];
for (var i = 0; i < cells.length; i++)
{
if (filter(cells[i]))
{
result.push(cells[i]);
}
}
}
return result;
};
/**
* Function: getDescendants
*
* Returns all descendants of the given cell and the cell itself in an array.
*
* Parameters:
*
* parent - <mxCell> whose descendants should be returned.
*/
mxGraphModel.prototype.getDescendants = function(parent)
{
return this.filterDescendants(null, parent);
};
/**
* Function: filterDescendants
*
* Visits all cells recursively and applies the specified filter function
* to each cell. If the function returns true then the cell is added
* to the resulting array. The parent and result paramters are optional.
* If parent is not specified then the recursion starts at <root>.
*
* Example:
* The following example extracts all vertices from a given model:
* (code)
* var filter = function(cell)
* {
* return model.isVertex(cell);
* }
* var vertices = model.filterDescendants(filter);
* (end)
*
* Parameters:
*
* filter - JavaScript function that takes an <mxCell> as an argument
* and returns a boolean.
* parent - Optional <mxCell> that is used as the root of the recursion.
*/
mxGraphModel.prototype.filterDescendants = function(filter, parent)
{
// Creates a new array for storing the result
var result = [];
// Recursion starts at the root of the model
parent = parent || this.getRoot();
// Checks if the filter returns true for the cell
// and adds it to the result array
if (filter == null || filter(parent))
{
result.push(parent);
}
// Visits the children of the cell
var childCount = this.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = this.getChildAt(parent, i);
result = result.concat(this.filterDescendants(filter, child));
}
return result;
};
/**
* Function: getRoot
*
* Returns the root of the model or the topmost parent of the given cell.
*
* Parameters:
*
* cell - Optional <mxCell> that specifies the child.
*/
mxGraphModel.prototype.getRoot = function(cell)
{
var root = cell || this.root;
if (cell != null)
{
while (cell != null)
{
root = cell;
cell = this.getParent(cell);
}
}
return root;
};
/**
* Function: setRoot
*
* Sets the <root> of the model using <mxRootChange> and adds the change to
* the current transaction. This resets all datastructures in the model and
* is the preferred way of clearing an existing model. Returns the new
* root.
*
* Example:
*
* (code)
* var root = new mxCell();
* root.insert(new mxCell());
* model.setRoot(root);
* (end)
*
* Parameters:
*
* root - <mxCell> that specifies the new root.
*/
mxGraphModel.prototype.setRoot = function(root)
{
this.execute(new mxRootChange(this, root));
return root;
};
/**
* Function: rootChanged
*
* Inner callback to change the root of the model and update the internal
* datastructures, such as <cells> and <nextId>. Returns the previous root.
*
* Parameters:
*
* root - <mxCell> that specifies the new root.
*/
mxGraphModel.prototype.rootChanged = function(root)
{
var oldRoot = this.root;
this.root = root;
// Resets counters and datastructures
this.nextId = 0;
this.cells = null;
this.cellAdded(root);
return oldRoot;
};
/**
* Function: isRoot
*
* Returns true if the given cell is the root of the model and a non-null
* value.
*
* Parameters:
*
* cell - <mxCell> that represents the possible root.
*/
mxGraphModel.prototype.isRoot = function(cell)
{
return cell != null && this.root == cell;
};
/**
* Function: isLayer
*
* Returns true if <isRoot> returns true for the parent of the given cell.
*
* Parameters:
*
* cell - <mxCell> that represents the possible layer.
*/
mxGraphModel.prototype.isLayer = function(cell)
{
return this.isRoot(this.getParent(cell));
};
/**
* Function: isAncestor
*
* Returns true if the given parent is an ancestor of the given child. Note
* returns true if child == parent.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent.
* child - <mxCell> that specifies the child.
*/
mxGraphModel.prototype.isAncestor = function(parent, child)
{
while (child != null && child != parent)
{
child = this.getParent(child);
}
return child == parent;
};
/**
* Function: contains
*
* Returns true if the model contains the given <mxCell>.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell.
*/
mxGraphModel.prototype.contains = function(cell)
{
return this.isAncestor(this.root, cell);
};
/**
* Function: getParent
*
* Returns the parent of the given cell.
*
* Parameters:
*
* cell - <mxCell> whose parent should be returned.
*/
mxGraphModel.prototype.getParent = function(cell)
{
return (cell != null) ? cell.getParent() : null;
};
/**
* Function: add
*
* Adds the specified child to the parent at the given index using
* <mxChildChange> and adds the change to the current transaction. If no
* index is specified then the child is appended to the parent's array of
* children. Returns the inserted child.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent to contain the child.
* child - <mxCell> that specifies the child to be inserted.
* index - Optional integer that specifies the index of the child.
*/
mxGraphModel.prototype.add = function(parent, child, index)
{
if (child != parent && parent != null && child != null)
{
// Appends the child if no index was specified
if (index == null)
{
index = this.getChildCount(parent);
}
var parentChanged = parent != this.getParent(child);
this.execute(new mxChildChange(this, parent, child, index));
// Maintains the edges parents by moving the edges
// into the nearest common ancestor of its terminals
if (this.maintainEdgeParent && parentChanged)
{
this.updateEdgeParents(child);
}
}
return child;
};
/**
* Function: cellAdded
*
* Inner callback to update <cells> when a cell has been added. This
* implementation resolves collisions by creating new Ids. To change the
* ID of a cell after it was inserted into the model, use the following
* code:
*
* (code
* delete model.cells[cell.getId()];
* cell.setId(newId);
* model.cells[cell.getId()] = cell;
* (end)
*
* If the change of the ID should be part of the command history, then the
* cell should be removed from the model and a clone with the new ID should
* be reinserted into the model instead.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell that has been added.
*/
mxGraphModel.prototype.cellAdded = function(cell)
{
if (cell != null)
{
// Creates an Id for the cell if not Id exists
if (cell.getId() == null && this.createIds)
{
cell.setId(this.createId(cell));
}
if (cell.getId() != null)
{
var collision = this.getCell(cell.getId());
if (collision != cell)
{
// Creates new Id for the cell
// as long as there is a collision
while (collision != null)
{
cell.setId(this.createId(cell));
collision = this.getCell(cell.getId());
}
// Lazily creates the cells dictionary
if (this.cells == null)
{
this.cells = new Object();
}
this.cells[cell.getId()] = cell;
}
}
// Makes sure IDs of deleted cells are not reused
if (mxUtils.isNumeric(cell.getId()))
{
this.nextId = Math.max(this.nextId, cell.getId());
}
// Recursively processes child cells
var childCount = this.getChildCount(cell);
for (var i=0; i<childCount; i++)
{
this.cellAdded(this.getChildAt(cell, i));
}
}
};
/**
* Function: createId
*
* Hook method to create an Id for the specified cell. This implementation
* concatenates <prefix>, id and <postfix> to create the Id and increments
* <nextId>. The cell is ignored by this implementation, but can be used in
* overridden methods to prefix the Ids with eg. the cell type.
*
* Parameters:
*
* cell - <mxCell> to create the Id for.
*/
mxGraphModel.prototype.createId = function(cell)
{
var id = this.nextId;
this.nextId++;
return this.prefix + id + this.postfix;
};
/**
* Function: updateEdgeParents
*
* Updates the parent for all edges that are connected to cell or one of
* its descendants using <updateEdgeParent>.
*/
mxGraphModel.prototype.updateEdgeParents = function(cell, root)
{
// Gets the topmost node of the hierarchy
root = root || this.getRoot(cell);
// Updates edges on children first
var childCount = this.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = this.getChildAt(cell, i);
this.updateEdgeParents(child, root);
}
// Updates the parents of all connected edges
var edgeCount = this.getEdgeCount(cell);
var edges = [];
for (var i = 0; i < edgeCount; i++)
{
edges.push(this.getEdgeAt(cell, i));
}
for (var i = 0; i < edges.length; i++)
{
var edge = edges[i];
// Updates edge parent if edge and child have
// a common root node (does not need to be the
// model root node)
if (this.isAncestor(root, edge))
{
this.updateEdgeParent(edge, root);
}
}
};
/**
* Function: updateEdgeParent
*
* Inner callback to update the parent of the specified <mxCell> to the
* nearest-common-ancestor of its two terminals.
*
* Parameters:
*
* edge - <mxCell> that specifies the edge.
* root - <mxCell> that represents the current root of the model.
*/
mxGraphModel.prototype.updateEdgeParent = function(edge, root)
{
var source = this.getTerminal(edge, true);
var target = this.getTerminal(edge, false);
var cell = null;
// Uses the first non-relative descendants of the source terminal
while (source != null && !this.isEdge(source) &&
source.geometry != null && source.geometry.relative)
{
source = this.getParent(source);
}
// Uses the first non-relative descendants of the target terminal
while (target != null && this.ignoreRelativeEdgeParent &&
!this.isEdge(target) && target.geometry != null &&
target.geometry.relative)
{
target = this.getParent(target);
}
if (this.isAncestor(root, source) && this.isAncestor(root, target))
{
if (source == target)
{
cell = this.getParent(source);
}
else
{
cell = this.getNearestCommonAncestor(source, target);
}
if (cell != null && (this.getParent(cell) != this.root ||
this.isAncestor(cell, edge)) && this.getParent(edge) != cell)
{
var geo = this.getGeometry(edge);
if (geo != null)
{
var origin1 = this.getOrigin(this.getParent(edge));
var origin2 = this.getOrigin(cell);
var dx = origin2.x - origin1.x;
var dy = origin2.y - origin1.y;
geo = geo.clone();
geo.translate(-dx, -dy);
this.setGeometry(edge, geo);
}
this.add(cell, edge, this.getChildCount(cell));
}
}
};
/**
* Function: getOrigin
*
* Returns the absolute, accumulated origin for the children inside the
* given parent as an <mxPoint>.
*/
mxGraphModel.prototype.getOrigin = function(cell)
{
var result = null;
if (cell != null)
{
result = this.getOrigin(this.getParent(cell));
if (!this.isEdge(cell))
{
var geo = this.getGeometry(cell);
if (geo != null)
{
result.x += geo.x;
result.y += geo.y;
}
}
}
else
{
result = new mxPoint();
}
return result;
};
/**
* Function: getNearestCommonAncestor
*
* Returns the nearest common ancestor for the specified cells.
*
* Parameters:
*
* cell1 - <mxCell> that specifies the first cell in the tree.
* cell2 - <mxCell> that specifies the second cell in the tree.
*/
mxGraphModel.prototype.getNearestCommonAncestor = function(cell1, cell2)
{
if (cell1 != null && cell2 != null)
{
// Creates the cell path for the second cell
var path = mxCellPath.create(cell2);
if (path != null && path.length > 0)
{
// Bubbles through the ancestors of the first
// cell to find the nearest common ancestor.
var cell = cell1;
var current = mxCellPath.create(cell);
// Inverts arguments
if (path.length < current.length)
{
cell = cell2;
var tmp = current;
current = path;
path = tmp;
}
while (cell != null)
{
var parent = this.getParent(cell);
// Checks if the cell path is equal to the beginning of the given cell path
if (path.indexOf(current + mxCellPath.PATH_SEPARATOR) == 0 && parent != null)
{
return cell;
}
current = mxCellPath.getParentPath(current);
cell = parent;
}
}
}
return null;
};
/**
* Function: remove
*
* Removes the specified cell from the model using <mxChildChange> and adds
* the change to the current transaction. This operation will remove the
* cell and all of its children from the model. Returns the removed cell.
*
* Parameters:
*
* cell - <mxCell> that should be removed.
*/
mxGraphModel.prototype.remove = function(cell)
{
if (cell == this.root)
{
this.setRoot(null);
}
else if (this.getParent(cell) != null)
{
this.execute(new mxChildChange(this, null, cell));
}
return cell;
};
/**
* Function: cellRemoved
*
* Inner callback to update <cells> when a cell has been removed.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell that has been removed.
*/
mxGraphModel.prototype.cellRemoved = function(cell)
{
if (cell != null && this.cells != null)
{
// Recursively processes child cells
var childCount = this.getChildCount(cell);
for (var i = childCount - 1; i >= 0; i--)
{
this.cellRemoved(this.getChildAt(cell, i));
}
// Removes the dictionary entry for the cell
if (this.cells != null && cell.getId() != null)
{
delete this.cells[cell.getId()];
}
}
};
/**
* Function: parentForCellChanged
*
* Inner callback to update the parent of a cell using <mxCell.insert>
* on the parent and return the previous parent.
*
* Parameters:
*
* cell - <mxCell> to update the parent for.
* parent - <mxCell> that specifies the new parent of the cell.
* index - Optional integer that defines the index of the child
* in the parent's child array.
*/
mxGraphModel.prototype.parentForCellChanged = function(cell, parent, index)
{
var previous = this.getParent(cell);
if (parent != null)
{
if (parent != previous || previous.getIndex(cell) != index)
{
parent.insert(cell, index);
}
}
else if (previous != null)
{
var oldIndex = previous.getIndex(cell);
previous.remove(oldIndex);
}
// Adds or removes the cell from the model
var par = this.contains(parent);
var pre = this.contains(previous);
if (par && !pre)
{
this.cellAdded(cell);
}
else if (pre && !par)
{
this.cellRemoved(cell);
}
return previous;
};
/**
* Function: getChildCount
*
* Returns the number of children in the given cell.
*
* Parameters:
*
* cell - <mxCell> whose number of children should be returned.
*/
mxGraphModel.prototype.getChildCount = function(cell)
{
return (cell != null) ? cell.getChildCount() : 0;
};
/**
* Function: getChildAt
*
* Returns the child of the given <mxCell> at the given index.
*
* Parameters:
*
* cell - <mxCell> that represents the parent.
* index - Integer that specifies the index of the child to be returned.
*/
mxGraphModel.prototype.getChildAt = function(cell, index)
{
return (cell != null) ? cell.getChildAt(index) : null;
};
/**
* Function: getChildren
*
* Returns all children of the given <mxCell> as an array of <mxCells>. The
* return value should be only be read.
*
* Parameters:
*
* cell - <mxCell> the represents the parent.
*/
mxGraphModel.prototype.getChildren = function(cell)
{
return (cell != null) ? cell.children : null;
};
/**
* Function: getChildVertices
*
* Returns the child vertices of the given parent.
*
* Parameters:
*
* cell - <mxCell> whose child vertices should be returned.
*/
mxGraphModel.prototype.getChildVertices = function(parent)
{
return this.getChildCells(parent, true, false);
};
/**
* Function: getChildEdges
*
* Returns the child edges of the given parent.
*
* Parameters:
*
* cell - <mxCell> whose child edges should be returned.
*/
mxGraphModel.prototype.getChildEdges = function(parent)
{
return this.getChildCells(parent, false, true);
};
/**
* Function: getChildCells
*
* Returns the children of the given cell that are vertices and/or edges
* depending on the arguments.
*
* Parameters:
*
* cell - <mxCell> the represents the parent.
* vertices - Boolean indicating if child vertices should be returned.
* Default is false.
* edges - Boolean indicating if child edges should be returned.
* Default is false.
*/
mxGraphModel.prototype.getChildCells = function(parent, vertices, edges)
{
vertices = (vertices != null) ? vertices : false;
edges = (edges != null) ? edges : false;
var childCount = this.getChildCount(parent);
var result = [];
for (var i = 0; i < childCount; i++)
{
var child = this.getChildAt(parent, i);
if ((!edges && !vertices) || (edges && this.isEdge(child)) ||
(vertices && this.isVertex(child)))
{
result.push(child);
}
}
return result;
};
/**
* Function: getTerminal
*
* Returns the source or target <mxCell> of the given edge depending on the
* value of the boolean parameter.
*
* Parameters:
*
* edge - <mxCell> that specifies the edge.
* isSource - Boolean indicating which end of the edge should be returned.
*/
mxGraphModel.prototype.getTerminal = function(edge, isSource)
{
return (edge != null) ? edge.getTerminal(isSource) : null;
};
/**
* Function: setTerminal
*
* Sets the source or target terminal of the given <mxCell> using
* <mxTerminalChange> and adds the change to the current transaction.
* This implementation updates the parent of the edge using <updateEdgeParent>
* if required.
*
* Parameters:
*
* edge - <mxCell> that specifies the edge.
* terminal - <mxCell> that specifies the new terminal.
* isSource - Boolean indicating if the terminal is the new source or
* target terminal of the edge.
*/
mxGraphModel.prototype.setTerminal = function(edge, terminal, isSource)
{
var terminalChanged = terminal != this.getTerminal(edge, isSource);
this.execute(new mxTerminalChange(this, edge, terminal, isSource));
if (this.maintainEdgeParent && terminalChanged)
{
this.updateEdgeParent(edge, this.getRoot());
}
return terminal;
};
/**
* Function: setTerminals
*
* Sets the source and target <mxCell> of the given <mxCell> in a single
* transaction using <setTerminal> for each end of the edge.
*
* Parameters:
*
* edge - <mxCell> that specifies the edge.
* source - <mxCell> that specifies the new source terminal.
* target - <mxCell> that specifies the new target terminal.
*/
mxGraphModel.prototype.setTerminals = function(edge, source, target)
{
this.beginUpdate();
try
{
this.setTerminal(edge, source, true);
this.setTerminal(edge, target, false);
}
finally
{
this.endUpdate();
}
};
/**
* Function: terminalForCellChanged
*
* Inner helper function to update the terminal of the edge using
* <mxCell.insertEdge> and return the previous terminal.
*
* Parameters:
*
* edge - <mxCell> that specifies the edge to be updated.
* terminal - <mxCell> that specifies the new terminal.
* isSource - Boolean indicating if the terminal is the new source or
* target terminal of the edge.
*/
mxGraphModel.prototype.terminalForCellChanged = function(edge, terminal, isSource)
{
var previous = this.getTerminal(edge, isSource);
if (terminal != null)
{
terminal.insertEdge(edge, isSource);
}
else if (previous != null)
{
previous.removeEdge(edge, isSource);
}
return previous;
};
/**
* Function: getEdgeCount
*
* Returns the number of distinct edges connected to the given cell.
*
* Parameters:
*
* cell - <mxCell> that represents the vertex.
*/
mxGraphModel.prototype.getEdgeCount = function(cell)
{
return (cell != null) ? cell.getEdgeCount() : 0;
};
/**
* Function: getEdgeAt
*
* Returns the edge of cell at the given index.
*
* Parameters:
*
* cell - <mxCell> that specifies the vertex.
* index - Integer that specifies the index of the edge
* to return.
*/
mxGraphModel.prototype.getEdgeAt = function(cell, index)
{
return (cell != null) ? cell.getEdgeAt(index) : null;
};
/**
* Function: getDirectedEdgeCount
*
* Returns the number of incoming or outgoing edges, ignoring the given
* edge.
*
* Parameters:
*
* cell - <mxCell> whose edge count should be returned.
* outgoing - Boolean that specifies if the number of outgoing or
* incoming edges should be returned.
* ignoredEdge - <mxCell> that represents an edge to be ignored.
*/
mxGraphModel.prototype.getDirectedEdgeCount = function(cell, outgoing, ignoredEdge)
{
var count = 0;
var edgeCount = this.getEdgeCount(cell);
for (var i = 0; i < edgeCount; i++)
{
var edge = this.getEdgeAt(cell, i);
if (edge != ignoredEdge && this.getTerminal(edge, outgoing) == cell)
{
count++;
}
}
return count;
};
/**
* Function: getConnections
*
* Returns all edges of the given cell without loops.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
*
*/
mxGraphModel.prototype.getConnections = function(cell)
{
return this.getEdges(cell, true, true, false);
};
/**
* Function: getIncomingEdges
*
* Returns the incoming edges of the given cell without loops.
*
* Parameters:
*
* cell - <mxCell> whose incoming edges should be returned.
*
*/
mxGraphModel.prototype.getIncomingEdges = function(cell)
{
return this.getEdges(cell, true, false, false);
};
/**
* Function: getOutgoingEdges
*
* Returns the outgoing edges of the given cell without loops.
*
* Parameters:
*
* cell - <mxCell> whose outgoing edges should be returned.
*
*/
mxGraphModel.prototype.getOutgoingEdges = function(cell)
{
return this.getEdges(cell, false, true, false);
};
/**
* Function: getEdges
*
* Returns all distinct edges connected to this cell as a new array of
* <mxCells>. If at least one of incoming or outgoing is true, then loops
* are ignored, otherwise if both are false, then all edges connected to
* the given cell are returned including loops.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell.
* incoming - Optional boolean that specifies if incoming edges should be
* returned. Default is true.
* outgoing - Optional boolean that specifies if outgoing edges should be
* returned. Default is true.
* includeLoops - Optional boolean that specifies if loops should be returned.
* Default is true.
*/
mxGraphModel.prototype.getEdges = function(cell, incoming, outgoing, includeLoops)
{
incoming = (incoming != null) ? incoming : true;
outgoing = (outgoing != null) ? outgoing : true;
includeLoops = (includeLoops != null) ? includeLoops : true;
var edgeCount = this.getEdgeCount(cell);
var result = [];
for (var i = 0; i < edgeCount; i++)
{
var edge = this.getEdgeAt(cell, i);
var source = this.getTerminal(edge, true);
var target = this.getTerminal(edge, false);
if ((includeLoops && source == target) || ((source != target) && ((incoming && target == cell) ||
(outgoing && source == cell))))
{
result.push(edge);
}
}
return result;
};
/**
* Function: getEdgesBetween
*
* Returns all edges between the given source and target pair. If directed
* is true, then only edges from the source to the target are returned,
* otherwise, all edges between the two cells are returned.
*
* Parameters:
*
* source - <mxCell> that defines the source terminal of the edge to be
* returned.
* target - <mxCell> that defines the target terminal of the edge to be
* returned.
* directed - Optional boolean that specifies if the direction of the
* edge should be taken into account. Default is false.
*/
mxGraphModel.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var tmp1 = this.getEdgeCount(source);
var tmp2 = this.getEdgeCount(target);
// Assumes the source has less connected edges
var terminal = source;
var edgeCount = tmp1;
// Uses the smaller array of connected edges
// for searching the edge
if (tmp2 < tmp1)
{
edgeCount = tmp2;
terminal = target;
}
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edgeCount; i++)
{
var edge = this.getEdgeAt(terminal, i);
var src = this.getTerminal(edge, true);
var trg = this.getTerminal(edge, false);
var directedMatch = (src == source) && (trg == target);
var oppositeMatch = (trg == source) && (src == target);
if (directedMatch || (!directed && oppositeMatch))
{
result.push(edge);
}
}
return result;
};
/**
* Function: getOpposites
*
* Returns all opposite vertices wrt terminal for the given edges, only
* returning sources and/or targets as specified. The result is returned
* as an array of <mxCells>.
*
* Parameters:
*
* edges - Array of <mxCells> that contain the edges to be examined.
* terminal - <mxCell> that specifies the known end of the edges.
* sources - Boolean that specifies if source terminals should be contained
* in the result. Default is true.
* targets - Boolean that specifies if target terminals should be contained
* in the result. Default is true.
*/
mxGraphModel.prototype.getOpposites = function(edges, terminal, sources, targets)
{
sources = (sources != null) ? sources : true;
targets = (targets != null) ? targets : true;
var terminals = [];
if (edges != null)
{
for (var i = 0; i < edges.length; i++)
{
var source = this.getTerminal(edges[i], true);
var target = this.getTerminal(edges[i], false);
// Checks if the terminal is the source of
// the edge and if the target should be
// stored in the result
if (source == terminal && target != null && target != terminal && targets)
{
terminals.push(target);
}
// Checks if the terminal is the taget of
// the edge and if the source should be
// stored in the result
else if (target == terminal && source != null && source != terminal && sources)
{
terminals.push(source);
}
}
}
return terminals;
};
/**
* Function: getTopmostCells
*
* Returns the topmost cells of the hierarchy in an array that contains no
* descendants for each <mxCell> that it contains. Duplicates should be
* removed in the cells array to improve performance.
*
* Parameters:
*
* cells - Array of <mxCells> whose topmost ancestors should be returned.
*/
mxGraphModel.prototype.getTopmostCells = function(cells)
{
var dict = new mxDictionary();
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
for (var i = 0; i < cells.length; i++)
{
var cell = cells[i];
var topmost = true;
var parent = this.getParent(cell);
while (parent != null)
{
if (dict.get(parent))
{
topmost = false;
break;
}
parent = this.getParent(parent);
}
if (topmost)
{
tmp.push(cell);
}
}
return tmp;
};
/**
* Function: isVertex
*
* Returns true if the given cell is a vertex.
*
* Parameters:
*
* cell - <mxCell> that represents the possible vertex.
*/
mxGraphModel.prototype.isVertex = function(cell)
{
return (cell != null) ? cell.isVertex() : false;
};
/**
* Function: isEdge
*
* Returns true if the given cell is an edge.
*
* Parameters:
*
* cell - <mxCell> that represents the possible edge.
*/
mxGraphModel.prototype.isEdge = function(cell)
{
return (cell != null) ? cell.isEdge() : false;
};
/**
* Function: isConnectable
*
* Returns true if the given <mxCell> is connectable. If <edgesConnectable>
* is false, then this function returns false for all edges else it returns
* the return value of <mxCell.isConnectable>.
*
* Parameters:
*
* cell - <mxCell> whose connectable state should be returned.
*/
mxGraphModel.prototype.isConnectable = function(cell)
{
return (cell != null) ? cell.isConnectable() : false;
};
/**
* Function: getValue
*
* Returns the user object of the given <mxCell> using <mxCell.getValue>.
*
* Parameters:
*
* cell - <mxCell> whose user object should be returned.
*/
mxGraphModel.prototype.getValue = function(cell)
{
return (cell != null) ? cell.getValue() : null;
};
/**
* Function: setValue
*
* Sets the user object of then given <mxCell> using <mxValueChange>
* and adds the change to the current transaction.
*
* Parameters:
*
* cell - <mxCell> whose user object should be changed.
* value - Object that defines the new user object.
*/
mxGraphModel.prototype.setValue = function(cell, value)
{
this.execute(new mxValueChange(this, cell, value));
return value;
};
/**
* Function: valueForCellChanged
*
* Inner callback to update the user object of the given <mxCell>
* using <mxCell.valueChanged> and return the previous value,
* that is, the return value of <mxCell.valueChanged>.
*
* To change a specific attribute in an XML node, the following code can be
* used.
*
* (code)
* graph.getModel().valueForCellChanged = function(cell, value)
* {
* var previous = cell.value.getAttribute('label');
* cell.value.setAttribute('label', value);
*
* return previous;
* };
* (end)
*/
mxGraphModel.prototype.valueForCellChanged = function(cell, value)
{
return cell.valueChanged(value);
};
/**
* Function: getGeometry
*
* Returns the <mxGeometry> of the given <mxCell>.
*
* Parameters:
*
* cell - <mxCell> whose geometry should be returned.
*/
mxGraphModel.prototype.getGeometry = function(cell)
{
return (cell != null) ? cell.getGeometry() : null;
};
/**
* Function: setGeometry
*
* Sets the <mxGeometry> of the given <mxCell>. The actual update
* of the cell is carried out in <geometryForCellChanged>. The
* <mxGeometryChange> action is used to encapsulate the change.
*
* Parameters:
*
* cell - <mxCell> whose geometry should be changed.
* geometry - <mxGeometry> that defines the new geometry.
*/
mxGraphModel.prototype.setGeometry = function(cell, geometry)
{
if (geometry != this.getGeometry(cell))
{
this.execute(new mxGeometryChange(this, cell, geometry));
}
return geometry;
};
/**
* Function: geometryForCellChanged
*
* Inner callback to update the <mxGeometry> of the given <mxCell> using
* <mxCell.setGeometry> and return the previous <mxGeometry>.
*/
mxGraphModel.prototype.geometryForCellChanged = function(cell, geometry)
{
var previous = this.getGeometry(cell);
cell.setGeometry(geometry);
return previous;
};
/**
* Function: getStyle
*
* Returns the style of the given <mxCell>.
*
* Parameters:
*
* cell - <mxCell> whose style should be returned.
*/
mxGraphModel.prototype.getStyle = function(cell)
{
return (cell != null) ? cell.getStyle() : null;
};
/**
* Function: setStyle
*
* Sets the style of the given <mxCell> using <mxStyleChange> and
* adds the change to the current transaction.
*
* Parameters:
*
* cell - <mxCell> whose style should be changed.
* style - String of the form [stylename;|key=value;] to specify
* the new cell style.
*/
mxGraphModel.prototype.setStyle = function(cell, style)
{
if (style != this.getStyle(cell))
{
this.execute(new mxStyleChange(this, cell, style));
}
return style;
};
/**
* Function: styleForCellChanged
*
* Inner callback to update the style of the given <mxCell>
* using <mxCell.setStyle> and return the previous style.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell to be updated.
* style - String of the form [stylename;|key=value;] to specify
* the new cell style.
*/
mxGraphModel.prototype.styleForCellChanged = function(cell, style)
{
var previous = this.getStyle(cell);
cell.setStyle(style);
return previous;
};
/**
* Function: isCollapsed
*
* Returns true if the given <mxCell> is collapsed.
*
* Parameters:
*
* cell - <mxCell> whose collapsed state should be returned.
*/
mxGraphModel.prototype.isCollapsed = function(cell)
{
return (cell != null) ? cell.isCollapsed() : false;
};
/**
* Function: setCollapsed
*
* Sets the collapsed state of the given <mxCell> using <mxCollapseChange>
* and adds the change to the current transaction.
*
* Parameters:
*
* cell - <mxCell> whose collapsed state should be changed.
* collapsed - Boolean that specifies the new collpased state.
*/
mxGraphModel.prototype.setCollapsed = function(cell, collapsed)
{
if (collapsed != this.isCollapsed(cell))
{
this.execute(new mxCollapseChange(this, cell, collapsed));
}
return collapsed;
};
/**
* Function: collapsedStateForCellChanged
*
* Inner callback to update the collapsed state of the
* given <mxCell> using <mxCell.setCollapsed> and return
* the previous collapsed state.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell to be updated.
* collapsed - Boolean that specifies the new collpased state.
*/
mxGraphModel.prototype.collapsedStateForCellChanged = function(cell, collapsed)
{
var previous = this.isCollapsed(cell);
cell.setCollapsed(collapsed);
return previous;
};
/**
* Function: isVisible
*
* Returns true if the given <mxCell> is visible.
*
* Parameters:
*
* cell - <mxCell> whose visible state should be returned.
*/
mxGraphModel.prototype.isVisible = function(cell)
{
return (cell != null) ? cell.isVisible() : false;
};
/**
* Function: setVisible
*
* Sets the visible state of the given <mxCell> using <mxVisibleChange> and
* adds the change to the current transaction.
*
* Parameters:
*
* cell - <mxCell> whose visible state should be changed.
* visible - Boolean that specifies the new visible state.
*/
mxGraphModel.prototype.setVisible = function(cell, visible)
{
if (visible != this.isVisible(cell))
{
this.execute(new mxVisibleChange(this, cell, visible));
}
return visible;
};
/**
* Function: visibleStateForCellChanged
*
* Inner callback to update the visible state of the
* given <mxCell> using <mxCell.setCollapsed> and return
* the previous visible state.
*
* Parameters:
*
* cell - <mxCell> that specifies the cell to be updated.
* visible - Boolean that specifies the new visible state.
*/
mxGraphModel.prototype.visibleStateForCellChanged = function(cell, visible)
{
var previous = this.isVisible(cell);
cell.setVisible(visible);
return previous;
};
/**
* Function: execute
*
* Executes the given edit and fires events if required. The edit object
* requires an execute function which is invoked. The edit is added to the
* <currentEdit> between <beginUpdate> and <endUpdate> calls, so that
* events will be fired if this execute is an individual transaction, that
* is, if no previous <beginUpdate> calls have been made without calling
* <endUpdate>. This implementation fires an <execute> event before
* executing the given change.
*
* Parameters:
*
* change - Object that described the change.
*/
mxGraphModel.prototype.execute = function(change)
{
change.execute();
this.beginUpdate();
this.currentEdit.add(change);
this.fireEvent(new mxEventObject(mxEvent.EXECUTE, 'change', change));
// New global executed event
this.fireEvent(new mxEventObject(mxEvent.EXECUTED, 'change', change));
this.endUpdate();
};
/**
* Function: beginUpdate
*
* Increments the <updateLevel> by one. The event notification
* is queued until <updateLevel> reaches 0 by use of
* <endUpdate>.
*
* All changes on <mxGraphModel> are transactional,
* that is, they are executed in a single undoable change
* on the model (without transaction isolation).
* Therefore, if you want to combine any
* number of changes into a single undoable change,
* you should group any two or more API calls that
* modify the graph model between <beginUpdate>
* and <endUpdate> calls as shown here:
*
* (code)
* var model = graph.getModel();
* var parent = graph.getDefaultParent();
* var index = model.getChildCount(parent);
* model.beginUpdate();
* try
* {
* model.add(parent, v1, index);
* model.add(parent, v2, index+1);
* }
* finally
* {
* model.endUpdate();
* }
* (end)
*
* Of course there is a shortcut for appending a
* sequence of cells into the default parent:
*
* (code)
* graph.addCells([v1, v2]).
* (end)
*/
mxGraphModel.prototype.beginUpdate = function()
{
this.updateLevel++;
this.fireEvent(new mxEventObject(mxEvent.BEGIN_UPDATE));
if (this.updateLevel == 1)
{
this.fireEvent(new mxEventObject(mxEvent.START_EDIT));
}
};
/**
* Function: endUpdate
*
* Decrements the <updateLevel> by one and fires an <undo>
* event if the <updateLevel> reaches 0. This function
* indirectly fires a <change> event by invoking the notify
* function on the <currentEdit> und then creates a new
* <currentEdit> using <createUndoableEdit>.
*
* The <undo> event is fired only once per edit, whereas
* the <change> event is fired whenever the notify
* function is invoked, that is, on undo and redo of
* the edit.
*/
mxGraphModel.prototype.endUpdate = function()
{
this.updateLevel--;
if (this.updateLevel == 0)
{
this.fireEvent(new mxEventObject(mxEvent.END_EDIT));
}
if (!this.endingUpdate)
{
this.endingUpdate = this.updateLevel == 0;
this.fireEvent(new mxEventObject(mxEvent.END_UPDATE, 'edit', this.currentEdit));
try
{
if (this.endingUpdate && !this.currentEdit.isEmpty())
{
this.fireEvent(new mxEventObject(mxEvent.BEFORE_UNDO, 'edit', this.currentEdit));
var tmp = this.currentEdit;
this.currentEdit = this.createUndoableEdit();
tmp.notify();
this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', tmp));
}
}
finally
{
this.endingUpdate = false;
}
}
};
/**
* Function: createUndoableEdit
*
* Creates a new <mxUndoableEdit> that implements the
* notify function to fire a <change> and <notify> event
* through the <mxUndoableEdit>'s source.
*
* Parameters:
*
* significant - Optional boolean that specifies if the edit to be created is
* significant. Default is true.
*/
mxGraphModel.prototype.createUndoableEdit = function(significant)
{
var edit = new mxUndoableEdit(this, (significant != null) ? significant : true);
edit.notify = function()
{
// LATER: Remove changes property (deprecated)
edit.source.fireEvent(new mxEventObject(mxEvent.CHANGE,
'edit', edit, 'changes', edit.changes));
edit.source.fireEvent(new mxEventObject(mxEvent.NOTIFY,
'edit', edit, 'changes', edit.changes));
};
return edit;
};
/**
* Function: mergeChildren
*
* Merges the children of the given cell into the given target cell inside
* this model. All cells are cloned unless there is a corresponding cell in
* the model with the same id, in which case the source cell is ignored and
* all edges are connected to the corresponding cell in this model. Edges
* are considered to have no identity and are always cloned unless the
* cloneAllEdges flag is set to false, in which case edges with the same
* id in the target model are reconnected to reflect the terminals of the
* source edges.
*/
mxGraphModel.prototype.mergeChildren = function(from, to, cloneAllEdges)
{
cloneAllEdges = (cloneAllEdges != null) ? cloneAllEdges : true;
this.beginUpdate();
try
{
var mapping = new Object();
this.mergeChildrenImpl(from, to, cloneAllEdges, mapping);
// Post-processes all edges in the mapping and
// reconnects the terminals to the corresponding
// cells in the target model
for (var key in mapping)
{
var cell = mapping[key];
var terminal = this.getTerminal(cell, true);
if (terminal != null)
{
terminal = mapping[mxCellPath.create(terminal)];
this.setTerminal(cell, terminal, true);
}
terminal = this.getTerminal(cell, false);
if (terminal != null)
{
terminal = mapping[mxCellPath.create(terminal)];
this.setTerminal(cell, terminal, false);
}
}
}
finally
{
this.endUpdate();
}
};
/**
* Function: mergeChildren
*
* Clones the children of the source cell into the given target cell in
* this model and adds an entry to the mapping that maps from the source
* cell to the target cell with the same id or the clone of the source cell
* that was inserted into this model.
*/
mxGraphModel.prototype.mergeChildrenImpl = function(from, to, cloneAllEdges, mapping)
{
this.beginUpdate();
try
{
var childCount = from.getChildCount();
for (var i = 0; i < childCount; i++)
{
var cell = from.getChildAt(i);
if (typeof(cell.getId) == 'function')
{
var id = cell.getId();
var target = (id != null && (!this.isEdge(cell) || !cloneAllEdges)) ?
this.getCell(id) : null;
// Clones and adds the child if no cell exists for the id
if (target == null)
{
var clone = cell.clone();
clone.setId(id);
// Sets the terminals from the original cell to the clone
// because the lookup uses strings not cells in JS
clone.setTerminal(cell.getTerminal(true), true);
clone.setTerminal(cell.getTerminal(false), false);
// Do *NOT* use model.add as this will move the edge away
// from the parent in updateEdgeParent if maintainEdgeParent
// is enabled in the target model
target = to.insert(clone);
this.cellAdded(target);
}
// Stores the mapping for later reconnecting edges
mapping[mxCellPath.create(cell)] = target;
// Recurses
this.mergeChildrenImpl(cell, target, cloneAllEdges, mapping);
}
}
}
finally
{
this.endUpdate();
}
};
/**
* Function: getParents
*
* Returns an array that represents the set (no duplicates) of all parents
* for the given array of cells.
*
* Parameters:
*
* cells - Array of cells whose parents should be returned.
*/
mxGraphModel.prototype.getParents = function(cells)
{
var parents = [];
if (cells != null)
{
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
var parent = this.getParent(cells[i]);
if (parent != null && !dict.get(parent))
{
dict.put(parent, true);
parents.push(parent);
}
}
}
return parents;
};
//
// Cell Cloning
//
/**
* Function: cloneCell
*
* Returns a deep clone of the given <mxCell> (including
* the children) which is created using <cloneCells>.
*
* Parameters:
*
* cell - <mxCell> to be cloned.
*/
mxGraphModel.prototype.cloneCell = function(cell)
{
if (cell != null)
{
return this.cloneCells([cell], true)[0];
}
return null;
};
/**
* Function: cloneCells
*
* Returns an array of clones for the given array of <mxCells>.
* Depending on the value of includeChildren, a deep clone is created for
* each cell. Connections are restored based if the corresponding
* cell is contained in the passed in array.
*
* Parameters:
*
* cells - Array of <mxCell> to be cloned.
* includeChildren - Boolean indicating if the cells should be cloned
* with all descendants.
* mapping - Optional mapping for existing clones.
*/
mxGraphModel.prototype.cloneCells = function(cells, includeChildren, mapping)
{
mapping = (mapping != null) ? mapping : new Object();
var clones = [];
for (var i = 0; i < cells.length; i++)
{
if (cells[i] != null)
{
clones.push(this.cloneCellImpl(cells[i], mapping, includeChildren));
}
else
{
clones.push(null);
}
}
for (var i = 0; i < clones.length; i++)
{
if (clones[i] != null)
{
this.restoreClone(clones[i], cells[i], mapping);
}
}
return clones;
};
/**
* Function: cloneCellImpl
*
* Inner helper method for cloning cells recursively.
*/
mxGraphModel.prototype.cloneCellImpl = function(cell, mapping, includeChildren)
{
var ident = mxObjectIdentity.get(cell);
var clone = mapping[ident];
if (clone == null)
{
clone = this.cellCloned(cell);
mapping[ident] = clone;
if (includeChildren)
{
var childCount = this.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var cloneChild = this.cloneCellImpl(
this.getChildAt(cell, i), mapping, true);
clone.insert(cloneChild);
}
}
}
return clone;
};
/**
* Function: cellCloned
*
* Hook for cloning the cell. This returns cell.clone() or
* any possible exceptions.
*/
mxGraphModel.prototype.cellCloned = function(cell)
{
return cell.clone();
};
/**
* Function: restoreClone
*
* Inner helper method for restoring the connections in
* a network of cloned cells.
*/
mxGraphModel.prototype.restoreClone = function(clone, cell, mapping)
{
var source = this.getTerminal(cell, true);
if (source != null)
{
var tmp = mapping[mxObjectIdentity.get(source)];
if (tmp != null)
{
tmp.insertEdge(clone, true);
}
}
var target = this.getTerminal(cell, false);
if (target != null)
{
var tmp = mapping[mxObjectIdentity.get(target)];
if (tmp != null)
{
tmp.insertEdge(clone, false);
}
}
var childCount = this.getChildCount(clone);
for (var i = 0; i < childCount; i++)
{
this.restoreClone(this.getChildAt(clone, i),
this.getChildAt(cell, i), mapping);
}
};
//
// Atomic changes
//
/**
* Class: mxRootChange
*
* Action to change the root in a model.
*
* Constructor: mxRootChange
*
* Constructs a change of the root in the
* specified model.
*/
function mxRootChange(model, root)
{
this.model = model;
this.root = root;
this.previous = root;
};
/**
* Function: execute
*
* Carries out a change of the root using
* <mxGraphModel.rootChanged>.
*/
mxRootChange.prototype.execute = function()
{
this.root = this.previous;
this.previous = this.model.rootChanged(this.previous);
};
/**
* Class: mxChildChange
*
* Action to add or remove a child in a model.
*
* Constructor: mxChildChange
*
* Constructs a change of a child in the
* specified model.
*/
function mxChildChange(model, parent, child, index)
{
this.model = model;
this.parent = parent;
this.previous = parent;
this.child = child;
this.index = index;
this.previousIndex = index;
};
/**
* Function: execute
*
* Changes the parent of <child> using
* <mxGraphModel.parentForCellChanged> and
* removes or restores the cell's
* connections.
*/
mxChildChange.prototype.execute = function()
{
if (this.child != null)
{
var tmp = this.model.getParent(this.child);
var tmp2 = (tmp != null) ? tmp.getIndex(this.child) : 0;
if (this.previous == null)
{
this.connect(this.child, false);
}
tmp = this.model.parentForCellChanged(
this.child, this.previous, this.previousIndex);
if (this.previous != null)
{
this.connect(this.child, true);
}
this.parent = this.previous;
this.previous = tmp;
this.index = this.previousIndex;
this.previousIndex = tmp2;
}
};
/**
* Function: disconnect
*
* Disconnects the given cell recursively from its
* terminals and stores the previous terminal in the
* cell's terminals.
*/
mxChildChange.prototype.connect = function(cell, isConnect)
{
isConnect = (isConnect != null) ? isConnect : true;
var source = cell.getTerminal(true);
var target = cell.getTerminal(false);
if (source != null)
{
if (isConnect)
{
this.model.terminalForCellChanged(cell, source, true);
}
else
{
this.model.terminalForCellChanged(cell, null, true);
}
}
if (target != null)
{
if (isConnect)
{
this.model.terminalForCellChanged(cell, target, false);
}
else
{
this.model.terminalForCellChanged(cell, null, false);
}
}
cell.setTerminal(source, true);
cell.setTerminal(target, false);
var childCount = this.model.getChildCount(cell);
for (var i=0; i<childCount; i++)
{
this.connect(this.model.getChildAt(cell, i), isConnect);
}
};
/**
* Class: mxTerminalChange
*
* Action to change a terminal in a model.
*
* Constructor: mxTerminalChange
*
* Constructs a change of a terminal in the
* specified model.
*/
function mxTerminalChange(model, cell, terminal, source)
{
this.model = model;
this.cell = cell;
this.terminal = terminal;
this.previous = terminal;
this.source = source;
};
/**
* Function: execute
*
* Changes the terminal of <cell> to <previous> using
* <mxGraphModel.terminalForCellChanged>.
*/
mxTerminalChange.prototype.execute = function()
{
if (this.cell != null)
{
this.terminal = this.previous;
this.previous = this.model.terminalForCellChanged(
this.cell, this.previous, this.source);
}
};
/**
* Class: mxValueChange
*
* Action to change a user object in a model.
*
* Constructor: mxValueChange
*
* Constructs a change of a user object in the
* specified model.
*/
function mxValueChange(model, cell, value)
{
this.model = model;
this.cell = cell;
this.value = value;
this.previous = value;
};
/**
* Function: execute
*
* Changes the value of <cell> to <previous> using
* <mxGraphModel.valueForCellChanged>.
*/
mxValueChange.prototype.execute = function()
{
if (this.cell != null)
{
this.value = this.previous;
this.previous = this.model.valueForCellChanged(
this.cell, this.previous);
}
};
/**
* Class: mxStyleChange
*
* Action to change a cell's style in a model.
*
* Constructor: mxStyleChange
*
* Constructs a change of a style in the
* specified model.
*/
function mxStyleChange(model, cell, style)
{
this.model = model;
this.cell = cell;
this.style = style;
this.previous = style;
};
/**
* Function: execute
*
* Changes the style of <cell> to <previous> using
* <mxGraphModel.styleForCellChanged>.
*/
mxStyleChange.prototype.execute = function()
{
if (this.cell != null)
{
this.style = this.previous;
this.previous = this.model.styleForCellChanged(
this.cell, this.previous);
}
};
/**
* Class: mxGeometryChange
*
* Action to change a cell's geometry in a model.
*
* Constructor: mxGeometryChange
*
* Constructs a change of a geometry in the
* specified model.
*/
function mxGeometryChange(model, cell, geometry)
{
this.model = model;
this.cell = cell;
this.geometry = geometry;
this.previous = geometry;
};
/**
* Function: execute
*
* Changes the geometry of <cell> ro <previous> using
* <mxGraphModel.geometryForCellChanged>.
*/
mxGeometryChange.prototype.execute = function()
{
if (this.cell != null)
{
this.geometry = this.previous;
this.previous = this.model.geometryForCellChanged(
this.cell, this.previous);
}
};
/**
* Class: mxCollapseChange
*
* Action to change a cell's collapsed state in a model.
*
* Constructor: mxCollapseChange
*
* Constructs a change of a collapsed state in the
* specified model.
*/
function mxCollapseChange(model, cell, collapsed)
{
this.model = model;
this.cell = cell;
this.collapsed = collapsed;
this.previous = collapsed;
};
/**
* Function: execute
*
* Changes the collapsed state of <cell> to <previous> using
* <mxGraphModel.collapsedStateForCellChanged>.
*/
mxCollapseChange.prototype.execute = function()
{
if (this.cell != null)
{
this.collapsed = this.previous;
this.previous = this.model.collapsedStateForCellChanged(
this.cell, this.previous);
}
};
/**
* Class: mxVisibleChange
*
* Action to change a cell's visible state in a model.
*
* Constructor: mxVisibleChange
*
* Constructs a change of a visible state in the
* specified model.
*/
function mxVisibleChange(model, cell, visible)
{
this.model = model;
this.cell = cell;
this.visible = visible;
this.previous = visible;
};
/**
* Function: execute
*
* Changes the visible state of <cell> to <previous> using
* <mxGraphModel.visibleStateForCellChanged>.
*/
mxVisibleChange.prototype.execute = function()
{
if (this.cell != null)
{
this.visible = this.previous;
this.previous = this.model.visibleStateForCellChanged(
this.cell, this.previous);
}
};
/**
* Class: mxCellAttributeChange
*
* Action to change the attribute of a cell's user object.
* There is no method on the graph model that uses this
* action. To use the action, you can use the code shown
* in the example below.
*
* Example:
*
* To change the attributeName in the cell's user object
* to attributeValue, use the following code:
*
* (code)
* model.beginUpdate();
* try
* {
* var edit = new mxCellAttributeChange(
* cell, attributeName, attributeValue);
* model.execute(edit);
* }
* finally
* {
* model.endUpdate();
* }
* (end)
*
* Constructor: mxCellAttributeChange
*
* Constructs a change of a attribute of the DOM node
* stored as the value of the given <mxCell>.
*/
function mxCellAttributeChange(cell, attribute, value)
{
this.cell = cell;
this.attribute = attribute;
this.value = value;
this.previous = value;
};
/**
* Function: execute
*
* Changes the attribute of the cell's user object by
* using <mxCell.setAttribute>.
*/
mxCellAttributeChange.prototype.execute = function()
{
if (this.cell != null)
{
var tmp = this.cell.getAttribute(this.attribute);
if (this.previous == null)
{
this.cell.value.removeAttribute(this.attribute);
}
else
{
this.cell.setAttribute(this.attribute, this.previous);
}
this.previous = tmp;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCell
*
* Cells are the elements of the graph model. They represent the state
* of the groups, vertices and edges in a graph.
*
* Custom attributes:
*
* For custom attributes we recommend using an XML node as the value of a cell.
* The following code can be used to create a cell with an XML node as the
* value:
*
* (code)
* var doc = mxUtils.createXmlDocument();
* var node = doc.createElement('MyNode')
* node.setAttribute('label', 'MyLabel');
* node.setAttribute('attribute1', 'value1');
* graph.insertVertex(graph.getDefaultParent(), null, node, 40, 40, 80, 30);
* (end)
*
* For the label to work, <mxGraph.convertValueToString> and
* <mxGraph.cellLabelChanged> should be overridden as follows:
*
* (code)
* graph.convertValueToString = function(cell)
* {
* if (mxUtils.isNode(cell.value))
* {
* return cell.getAttribute('label', '')
* }
* };
*
* var cellLabelChanged = graph.cellLabelChanged;
* graph.cellLabelChanged = function(cell, newValue, autoSize)
* {
* if (mxUtils.isNode(cell.value))
* {
* // Clones the value for correct undo/redo
* var elt = cell.value.cloneNode(true);
* elt.setAttribute('label', newValue);
* newValue = elt;
* }
*
* cellLabelChanged.apply(this, arguments);
* };
* (end)
*
* Callback: onInit
*
* Called from within the constructor.
*
* Constructor: mxCell
*
* Constructs a new cell to be used in a graph model.
* This method invokes <onInit> upon completion.
*
* Parameters:
*
* value - Optional object that represents the cell value.
* geometry - Optional <mxGeometry> that specifies the geometry.
* style - Optional formatted string that defines the style.
*/
function mxCell(value, geometry, style)
{
this.value = value;
this.setGeometry(geometry);
this.setStyle(style);
if (this.onInit != null)
{
this.onInit();
}
};
/**
* Variable: id
*
* Holds the Id. Default is null.
*/
mxCell.prototype.id = null;
/**
* Variable: value
*
* Holds the user object. Default is null.
*/
mxCell.prototype.value = null;
/**
* Variable: geometry
*
* Holds the <mxGeometry>. Default is null.
*/
mxCell.prototype.geometry = null;
/**
* Variable: style
*
* Holds the style as a string of the form [(stylename|key=value);]. Default is
* null.
*/
mxCell.prototype.style = null;
/**
* Variable: vertex
*
* Specifies whether the cell is a vertex. Default is false.
*/
mxCell.prototype.vertex = false;
/**
* Variable: edge
*
* Specifies whether the cell is an edge. Default is false.
*/
mxCell.prototype.edge = false;
/**
* Variable: connectable
*
* Specifies whether the cell is connectable. Default is true.
*/
mxCell.prototype.connectable = true;
/**
* Variable: visible
*
* Specifies whether the cell is visible. Default is true.
*/
mxCell.prototype.visible = true;
/**
* Variable: collapsed
*
* Specifies whether the cell is collapsed. Default is false.
*/
mxCell.prototype.collapsed = false;
/**
* Variable: parent
*
* Reference to the parent cell.
*/
mxCell.prototype.parent = null;
/**
* Variable: source
*
* Reference to the source terminal.
*/
mxCell.prototype.source = null;
/**
* Variable: target
*
* Reference to the target terminal.
*/
mxCell.prototype.target = null;
/**
* Variable: children
*
* Holds the child cells.
*/
mxCell.prototype.children = null;
/**
* Variable: edges
*
* Holds the edges.
*/
mxCell.prototype.edges = null;
/**
* Variable: mxTransient
*
* List of members that should not be cloned inside <clone>. This field is
* passed to <mxUtils.clone> and is not made persistent in <mxCellCodec>.
* This is not a convention for all classes, it is only used in this class
* to mark transient fields since transient modifiers are not supported by
* the language.
*/
mxCell.prototype.mxTransient = ['id', 'value', 'parent', 'source',
'target', 'children', 'edges'];
/**
* Function: getId
*
* Returns the Id of the cell as a string.
*/
mxCell.prototype.getId = function()
{
return this.id;
};
/**
* Function: setId
*
* Sets the Id of the cell to the given string.
*/
mxCell.prototype.setId = function(id)
{
this.id = id;
};
/**
* Function: getValue
*
* Returns the user object of the cell. The user
* object is stored in <value>.
*/
mxCell.prototype.getValue = function()
{
return this.value;
};
/**
* Function: setValue
*
* Sets the user object of the cell. The user object
* is stored in <value>.
*/
mxCell.prototype.setValue = function(value)
{
this.value = value;
};
/**
* Function: valueChanged
*
* Changes the user object after an in-place edit
* and returns the previous value. This implementation
* replaces the user object with the given value and
* returns the old user object.
*/
mxCell.prototype.valueChanged = function(newValue)
{
var previous = this.getValue();
this.setValue(newValue);
return previous;
};
/**
* Function: getGeometry
*
* Returns the <mxGeometry> that describes the <geometry>.
*/
mxCell.prototype.getGeometry = function()
{
return this.geometry;
};
/**
* Function: setGeometry
*
* Sets the <mxGeometry> to be used as the <geometry>.
*/
mxCell.prototype.setGeometry = function(geometry)
{
this.geometry = geometry;
};
/**
* Function: getStyle
*
* Returns a string that describes the <style>.
*/
mxCell.prototype.getStyle = function()
{
return this.style;
};
/**
* Function: setStyle
*
* Sets the string to be used as the <style>.
*/
mxCell.prototype.setStyle = function(style)
{
this.style = style;
};
/**
* Function: isVertex
*
* Returns true if the cell is a vertex.
*/
mxCell.prototype.isVertex = function()
{
return this.vertex != 0;
};
/**
* Function: setVertex
*
* Specifies if the cell is a vertex. This should only be assigned at
* construction of the cell and not be changed during its lifecycle.
*
* Parameters:
*
* vertex - Boolean that specifies if the cell is a vertex.
*/
mxCell.prototype.setVertex = function(vertex)
{
this.vertex = vertex;
};
/**
* Function: isEdge
*
* Returns true if the cell is an edge.
*/
mxCell.prototype.isEdge = function()
{
return this.edge != 0;
};
/**
* Function: setEdge
*
* Specifies if the cell is an edge. This should only be assigned at
* construction of the cell and not be changed during its lifecycle.
*
* Parameters:
*
* edge - Boolean that specifies if the cell is an edge.
*/
mxCell.prototype.setEdge = function(edge)
{
this.edge = edge;
};
/**
* Function: isConnectable
*
* Returns true if the cell is connectable.
*/
mxCell.prototype.isConnectable = function()
{
return this.connectable != 0;
};
/**
* Function: setConnectable
*
* Sets the connectable state.
*
* Parameters:
*
* connectable - Boolean that specifies the new connectable state.
*/
mxCell.prototype.setConnectable = function(connectable)
{
this.connectable = connectable;
};
/**
* Function: isVisible
*
* Returns true if the cell is visibile.
*/
mxCell.prototype.isVisible = function()
{
return this.visible != 0;
};
/**
* Function: setVisible
*
* Specifies if the cell is visible.
*
* Parameters:
*
* visible - Boolean that specifies the new visible state.
*/
mxCell.prototype.setVisible = function(visible)
{
this.visible = visible;
};
/**
* Function: isCollapsed
*
* Returns true if the cell is collapsed.
*/
mxCell.prototype.isCollapsed = function()
{
return this.collapsed != 0;
};
/**
* Function: setCollapsed
*
* Sets the collapsed state.
*
* Parameters:
*
* collapsed - Boolean that specifies the new collapsed state.
*/
mxCell.prototype.setCollapsed = function(collapsed)
{
this.collapsed = collapsed;
};
/**
* Function: getParent
*
* Returns the cell's parent.
*/
mxCell.prototype.getParent = function()
{
return this.parent;
};
/**
* Function: setParent
*
* Sets the parent cell.
*
* Parameters:
*
* parent - <mxCell> that represents the new parent.
*/
mxCell.prototype.setParent = function(parent)
{
this.parent = parent;
};
/**
* Function: getTerminal
*
* Returns the source or target terminal.
*
* Parameters:
*
* source - Boolean that specifies if the source terminal should be
* returned.
*/
mxCell.prototype.getTerminal = function(source)
{
return (source) ? this.source : this.target;
};
/**
* Function: setTerminal
*
* Sets the source or target terminal and returns the new terminal.
*
* Parameters:
*
* terminal - <mxCell> that represents the new source or target terminal.
* isSource - Boolean that specifies if the source or target terminal
* should be set.
*/
mxCell.prototype.setTerminal = function(terminal, isSource)
{
if (isSource)
{
this.source = terminal;
}
else
{
this.target = terminal;
}
return terminal;
};
/**
* Function: getChildCount
*
* Returns the number of child cells.
*/
mxCell.prototype.getChildCount = function()
{
return (this.children == null) ? 0 : this.children.length;
};
/**
* Function: getIndex
*
* Returns the index of the specified child in the child array.
*
* Parameters:
*
* child - Child whose index should be returned.
*/
mxCell.prototype.getIndex = function(child)
{
return mxUtils.indexOf(this.children, child);
};
/**
* Function: getChildAt
*
* Returns the child at the specified index.
*
* Parameters:
*
* index - Integer that specifies the child to be returned.
*/
mxCell.prototype.getChildAt = function(index)
{
return (this.children == null) ? null : this.children[index];
};
/**
* Function: insert
*
* Inserts the specified child into the child array at the specified index
* and updates the parent reference of the child. If not childIndex is
* specified then the child is appended to the child array. Returns the
* inserted child.
*
* Parameters:
*
* child - <mxCell> to be inserted or appended to the child array.
* index - Optional integer that specifies the index at which the child
* should be inserted into the child array.
*/
mxCell.prototype.insert = function(child, index)
{
if (child != null)
{
if (index == null)
{
index = this.getChildCount();
if (child.getParent() == this)
{
index--;
}
}
child.removeFromParent();
child.setParent(this);
if (this.children == null)
{
this.children = [];
this.children.push(child);
}
else
{
this.children.splice(index, 0, child);
}
}
return child;
};
/**
* Function: remove
*
* Removes the child at the specified index from the child array and
* returns the child that was removed. Will remove the parent reference of
* the child.
*
* Parameters:
*
* index - Integer that specifies the index of the child to be
* removed.
*/
mxCell.prototype.remove = function(index)
{
var child = null;
if (this.children != null && index >= 0)
{
child = this.getChildAt(index);
if (child != null)
{
this.children.splice(index, 1);
child.setParent(null);
}
}
return child;
};
/**
* Function: removeFromParent
*
* Removes the cell from its parent.
*/
mxCell.prototype.removeFromParent = function()
{
if (this.parent != null)
{
var index = this.parent.getIndex(this);
this.parent.remove(index);
}
};
/**
* Function: getEdgeCount
*
* Returns the number of edges in the edge array.
*/
mxCell.prototype.getEdgeCount = function()
{
return (this.edges == null) ? 0 : this.edges.length;
};
/**
* Function: getEdgeIndex
*
* Returns the index of the specified edge in <edges>.
*
* Parameters:
*
* edge - <mxCell> whose index in <edges> should be returned.
*/
mxCell.prototype.getEdgeIndex = function(edge)
{
return mxUtils.indexOf(this.edges, edge);
};
/**
* Function: getEdgeAt
*
* Returns the edge at the specified index in <edges>.
*
* Parameters:
*
* index - Integer that specifies the index of the edge to be returned.
*/
mxCell.prototype.getEdgeAt = function(index)
{
return (this.edges == null) ? null : this.edges[index];
};
/**
* Function: insertEdge
*
* Inserts the specified edge into the edge array and returns the edge.
* Will update the respective terminal reference of the edge.
*
* Parameters:
*
* edge - <mxCell> to be inserted into the edge array.
* isOutgoing - Boolean that specifies if the edge is outgoing.
*/
mxCell.prototype.insertEdge = function(edge, isOutgoing)
{
if (edge != null)
{
edge.removeFromTerminal(isOutgoing);
edge.setTerminal(this, isOutgoing);
if (this.edges == null ||
edge.getTerminal(!isOutgoing) != this ||
mxUtils.indexOf(this.edges, edge) < 0)
{
if (this.edges == null)
{
this.edges = [];
}
this.edges.push(edge);
}
}
return edge;
};
/**
* Function: removeEdge
*
* Removes the specified edge from the edge array and returns the edge.
* Will remove the respective terminal reference from the edge.
*
* Parameters:
*
* edge - <mxCell> to be removed from the edge array.
* isOutgoing - Boolean that specifies if the edge is outgoing.
*/
mxCell.prototype.removeEdge = function(edge, isOutgoing)
{
if (edge != null)
{
if (edge.getTerminal(!isOutgoing) != this &&
this.edges != null)
{
var index = this.getEdgeIndex(edge);
if (index >= 0)
{
this.edges.splice(index, 1);
}
}
edge.setTerminal(null, isOutgoing);
}
return edge;
};
/**
* Function: removeFromTerminal
*
* Removes the edge from its source or target terminal.
*
* Parameters:
*
* isSource - Boolean that specifies if the edge should be removed from its
* source or target terminal.
*/
mxCell.prototype.removeFromTerminal = function(isSource)
{
var terminal = this.getTerminal(isSource);
if (terminal != null)
{
terminal.removeEdge(this, isSource);
}
};
/**
* Function: hasAttribute
*
* Returns true if the user object is an XML node that contains the given
* attribute.
*
* Parameters:
*
* name - Name of the attribute.
*/
mxCell.prototype.hasAttribute = function(name)
{
var userObject = this.getValue();
return (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT && userObject.hasAttribute) ?
userObject.hasAttribute(name) : userObject.getAttribute(name) != null;
};
/**
* Function: getAttribute
*
* Returns the specified attribute from the user object if it is an XML
* node.
*
* Parameters:
*
* name - Name of the attribute whose value should be returned.
* defaultValue - Optional default value to use if the attribute has no
* value.
*/
mxCell.prototype.getAttribute = function(name, defaultValue)
{
var userObject = this.getValue();
var val = (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT) ?
userObject.getAttribute(name) : null;
return (val != null) ? val : defaultValue;
};
/**
* Function: setAttribute
*
* Sets the specified attribute on the user object if it is an XML node.
*
* Parameters:
*
* name - Name of the attribute whose value should be set.
* value - New value of the attribute.
*/
mxCell.prototype.setAttribute = function(name, value)
{
var userObject = this.getValue();
if (userObject != null &&
userObject.nodeType == mxConstants.NODETYPE_ELEMENT)
{
userObject.setAttribute(name, value);
}
};
/**
* Function: clone
*
* Returns a clone of the cell. Uses <cloneValue> to clone
* the user object. All fields in <mxTransient> are ignored
* during the cloning.
*/
mxCell.prototype.clone = function()
{
var clone = mxUtils.clone(this, this.mxTransient);
clone.setValue(this.cloneValue());
return clone;
};
/**
* Function: cloneValue
*
* Returns a clone of the cell's user object.
*/
mxCell.prototype.cloneValue = function()
{
var value = this.getValue();
if (value != null)
{
if (typeof(value.clone) == 'function')
{
value = value.clone();
}
else if (!isNaN(value.nodeType))
{
value = value.cloneNode(true);
}
}
return value;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGeometry
*
* Extends <mxRectangle> to represent the geometry of a cell.
*
* For vertices, the geometry consists of the x- and y-location, and the width
* and height. For edges, the geometry consists of the optional terminal- and
* control points. The terminal points are only required if an edge is
* unconnected, and are stored in the sourcePoint> and <targetPoint>
* variables, respectively.
*
* Example:
*
* If an edge is unconnected, that is, it has no source or target terminal,
* then a geometry with terminal points for a new edge can be defined as
* follows.
*
* (code)
* geometry.setTerminalPoint(new mxPoint(x1, y1), true);
* geometry.points = [new mxPoint(x2, y2)];
* geometry.setTerminalPoint(new mxPoint(x3, y3), false);
* (end)
*
* Control points are used regardless of the connected state of an edge and may
* be ignored or interpreted differently depending on the edge's <mxEdgeStyle>.
*
* To disable automatic reset of control points after a cell has been moved or
* resized, the the <mxGraph.resizeEdgesOnMove> and
* <mxGraph.resetEdgesOnResize> may be used.
*
* Edge Labels:
*
* Using the x- and y-coordinates of a cell's geometry, it is possible to
* position the label on edges on a specific location on the actual edge shape
* as it appears on the screen. The x-coordinate of an edge's geometry is used
* to describe the distance from the center of the edge from -1 to 1 with 0
* being the center of the edge and the default value. The y-coordinate of an
* edge's geometry is used to describe the absolute, orthogonal distance in
* pixels from that point. In addition, the <mxGeometry.offset> is used as an
* absolute offset vector from the resulting point.
*
* This coordinate system is applied if <relative> is true, otherwise the
* offset defines the absolute vector from the edge's center point to the
* label and the values for <x> and <y> are ignored.
*
* The width and height parameter for edge geometries can be used to set the
* label width and height (eg. for word wrapping).
*
* Ports:
*
* The term "port" refers to a relatively positioned, connectable child cell,
* which is used to specify the connection between the parent and another cell
* in the graph. Ports are typically modeled as vertices with relative
* geometries.
*
* Offsets:
*
* The <offset> field is interpreted in 3 different ways, depending on the cell
* and the geometry. For edges, the offset defines the absolute offset for the
* edge label. For relative geometries, the offset defines the absolute offset
* for the origin (top, left corner) of the vertex, otherwise the offset
* defines the absolute offset for the label inside the vertex or group.
*
* Constructor: mxGeometry
*
* Constructs a new object to describe the size and location of a vertex or
* the control points of an edge.
*/
function mxGeometry(x, y, width, height)
{
mxRectangle.call(this, x, y, width, height);
};
/**
* Extends mxRectangle.
*/
mxGeometry.prototype = new mxRectangle();
mxGeometry.prototype.constructor = mxGeometry;
/**
* Variable: TRANSLATE_CONTROL_POINTS
*
* Global switch to translate the points in translate. Default is true.
*/
mxGeometry.prototype.TRANSLATE_CONTROL_POINTS = true;
/**
* Variable: alternateBounds
*
* Stores alternate values for x, y, width and height in a rectangle. See
* <swap> to exchange the values. Default is null.
*/
mxGeometry.prototype.alternateBounds = null;
/**
* Variable: sourcePoint
*
* Defines the source <mxPoint> of the edge. This is used if the
* corresponding edge does not have a source vertex. Otherwise it is
* ignored. Default is null.
*/
mxGeometry.prototype.sourcePoint = null;
/**
* Variable: targetPoint
*
* Defines the target <mxPoint> of the edge. This is used if the
* corresponding edge does not have a target vertex. Otherwise it is
* ignored. Default is null.
*/
mxGeometry.prototype.targetPoint = null;
/**
* Variable: points
*
* Array of <mxPoints> which specifies the control points along the edge.
* These points are the intermediate points on the edge, for the endpoints
* use <targetPoint> and <sourcePoint> or set the terminals of the edge to
* a non-null value. Default is null.
*/
mxGeometry.prototype.points = null;
/**
* Variable: offset
*
* For edges, this holds the offset (in pixels) from the position defined
* by <x> and <y> on the edge. For relative geometries (for vertices), this
* defines the absolute offset from the point defined by the relative
* coordinates. For absolute geometries (for vertices), this defines the
* offset for the label. Default is null.
*/
mxGeometry.prototype.offset = null;
/**
* Variable: relative
*
* Specifies if the coordinates in the geometry are to be interpreted as
* relative coordinates. For edges, this is used to define the location of
* the edge label relative to the edge as rendered on the display. For
* vertices, this specifies the relative location inside the bounds of the
* parent cell.
*
* If this is false, then the coordinates are relative to the origin of the
* parent cell or, for edges, the edge label position is relative to the
* center of the edge as rendered on screen.
*
* Default is false.
*/
mxGeometry.prototype.relative = false;
/**
* Function: swap
*
* Swaps the x, y, width and height with the values stored in
* <alternateBounds> and puts the previous values into <alternateBounds> as
* a rectangle. This operation is carried-out in-place, that is, using the
* existing geometry instance. If this operation is called during a graph
* model transactional change, then the geometry should be cloned before
* calling this method and setting the geometry of the cell using
* <mxGraphModel.setGeometry>.
*/
mxGeometry.prototype.swap = function()
{
if (this.alternateBounds != null)
{
var old = new mxRectangle(
this.x, this.y, this.width, this.height);
this.x = this.alternateBounds.x;
this.y = this.alternateBounds.y;
this.width = this.alternateBounds.width;
this.height = this.alternateBounds.height;
this.alternateBounds = old;
}
};
/**
* Function: getTerminalPoint
*
* Returns the <mxPoint> representing the source or target point of this
* edge. This is only used if the edge has no source or target vertex.
*
* Parameters:
*
* isSource - Boolean that specifies if the source or target point
* should be returned.
*/
mxGeometry.prototype.getTerminalPoint = function(isSource)
{
return (isSource) ? this.sourcePoint : this.targetPoint;
};
/**
* Function: setTerminalPoint
*
* Sets the <sourcePoint> or <targetPoint> to the given <mxPoint> and
* returns the new point.
*
* Parameters:
*
* point - Point to be used as the new source or target point.
* isSource - Boolean that specifies if the source or target point
* should be set.
*/
mxGeometry.prototype.setTerminalPoint = function(point, isSource)
{
if (isSource)
{
this.sourcePoint = point;
}
else
{
this.targetPoint = point;
}
return point;
};
/**
* Function: rotate
*
* Rotates the geometry by the given angle around the given center. That is,
* <x> and <y> of the geometry, the <sourcePoint>, <targetPoint> and all
* <points> are translated by the given amount. <x> and <y> are only
* translated if <relative> is false.
*
* Parameters:
*
* angle - Number that specifies the rotation angle in degrees.
* cx - <mxPoint> that specifies the center of the rotation.
*/
mxGeometry.prototype.rotate = function(angle, cx)
{
var rad = mxUtils.toRadians(angle);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
// Rotates the geometry
if (!this.relative)
{
var ct = new mxPoint(this.getCenterX(), this.getCenterY());
var pt = mxUtils.getRotatedPoint(ct, cos, sin, cx);
this.x = Math.round(pt.x - this.width / 2);
this.y = Math.round(pt.y - this.height / 2);
}
// Rotates the source point
if (this.sourcePoint != null)
{
var pt = mxUtils.getRotatedPoint(this.sourcePoint, cos, sin, cx);
this.sourcePoint.x = Math.round(pt.x);
this.sourcePoint.y = Math.round(pt.y);
}
// Translates the target point
if (this.targetPoint != null)
{
var pt = mxUtils.getRotatedPoint(this.targetPoint, cos, sin, cx);
this.targetPoint.x = Math.round(pt.x);
this.targetPoint.y = Math.round(pt.y);
}
// Translate the control points
if (this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
var pt = mxUtils.getRotatedPoint(this.points[i], cos, sin, cx);
this.points[i].x = Math.round(pt.x);
this.points[i].y = Math.round(pt.y);
}
}
}
};
/**
* Function: translate
*
* Translates the geometry by the specified amount. That is, <x> and <y> of the
* geometry, the <sourcePoint>, <targetPoint> and all <points> are translated
* by the given amount. <x> and <y> are only translated if <relative> is false.
* If <TRANSLATE_CONTROL_POINTS> is false, then <points> are not modified by
* this function.
*
* Parameters:
*
* dx - Number that specifies the x-coordinate of the translation.
* dy - Number that specifies the y-coordinate of the translation.
*/
mxGeometry.prototype.translate = function(dx, dy)
{
dx = parseFloat(dx);
dy = parseFloat(dy);
// Translates the geometry
if (!this.relative)
{
this.x = parseFloat(this.x) + dx;
this.y = parseFloat(this.y) + dy;
}
// Translates the source point
if (this.sourcePoint != null)
{
this.sourcePoint.x = parseFloat(this.sourcePoint.x) + dx;
this.sourcePoint.y = parseFloat(this.sourcePoint.y) + dy;
}
// Translates the target point
if (this.targetPoint != null)
{
this.targetPoint.x = parseFloat(this.targetPoint.x) + dx;
this.targetPoint.y = parseFloat(this.targetPoint.y) + dy;
}
// Translate the control points
if (this.TRANSLATE_CONTROL_POINTS && this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
this.points[i].x = parseFloat(this.points[i].x) + dx;
this.points[i].y = parseFloat(this.points[i].y) + dy;
}
}
}
};
/**
* Function: scale
*
* Scales the geometry by the given amount. That is, <x> and <y> of the
* geometry, the <sourcePoint>, <targetPoint> and all <points> are scaled
* by the given amount. <x>, <y>, <width> and <height> are only scaled if
* <relative> is false. If <fixedAspect> is true, then the smaller value
* is used to scale the width and the height.
*
* Parameters:
*
* sx - Number that specifies the horizontal scale factor.
* sy - Number that specifies the vertical scale factor.
* fixedAspect - Optional boolean to keep the aspect ratio fixed.
*/
mxGeometry.prototype.scale = function(sx, sy, fixedAspect)
{
sx = parseFloat(sx);
sy = parseFloat(sy);
// Translates the source point
if (this.sourcePoint != null)
{
this.sourcePoint.x = parseFloat(this.sourcePoint.x) * sx;
this.sourcePoint.y = parseFloat(this.sourcePoint.y) * sy;
}
// Translates the target point
if (this.targetPoint != null)
{
this.targetPoint.x = parseFloat(this.targetPoint.x) * sx;
this.targetPoint.y = parseFloat(this.targetPoint.y) * sy;
}
// Translate the control points
if (this.points != null)
{
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i] != null)
{
this.points[i].x = parseFloat(this.points[i].x) * sx;
this.points[i].y = parseFloat(this.points[i].y) * sy;
}
}
}
// Translates the geometry
if (!this.relative)
{
this.x = parseFloat(this.x) * sx;
this.y = parseFloat(this.y) * sy;
if (fixedAspect)
{
sy = sx = Math.min(sx, sy);
}
this.width = parseFloat(this.width) * sx;
this.height = parseFloat(this.height) * sy;
}
};
/**
* Function: equals
*
* Returns true if the given object equals this geometry.
*/
mxGeometry.prototype.equals = function(obj)
{
return mxRectangle.prototype.equals.apply(this, arguments) &&
this.relative == obj.relative &&
((this.sourcePoint == null && obj.sourcePoint == null) || (this.sourcePoint != null && this.sourcePoint.equals(obj.sourcePoint))) &&
((this.targetPoint == null && obj.targetPoint == null) || (this.targetPoint != null && this.targetPoint.equals(obj.targetPoint))) &&
((this.points == null && obj.points == null) || (this.points != null && mxUtils.equalPoints(this.points, obj.points))) &&
((this.alternateBounds == null && obj.alternateBounds == null) || (this.alternateBounds != null && this.alternateBounds.equals(obj.alternateBounds))) &&
((this.offset == null && obj.offset == null) || (this.offset != null && this.offset.equals(obj.offset)));
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxCellPath =
{
/**
* Class: mxCellPath
*
* Implements a mechanism for temporary cell Ids.
*
* Variable: PATH_SEPARATOR
*
* Defines the separator between the path components. Default is ".".
*/
PATH_SEPARATOR: '.',
/**
* Function: create
*
* Creates the cell path for the given cell. The cell path is a
* concatenation of the indices of all ancestors on the (finite) path to
* the root, eg. "0.0.0.1".
*
* Parameters:
*
* cell - Cell whose path should be returned.
*/
create: function(cell)
{
var result = '';
if (cell != null)
{
var parent = cell.getParent();
while (parent != null)
{
var index = parent.getIndex(cell);
result = index + mxCellPath.PATH_SEPARATOR + result;
cell = parent;
parent = cell.getParent();
}
}
// Removes trailing separator
var n = result.length;
if (n > 1)
{
result = result.substring(0, n - 1);
}
return result;
},
/**
* Function: getParentPath
*
* Returns the path for the parent of the cell represented by the given
* path. Returns null if the given path has no parent.
*
* Parameters:
*
* path - Path whose parent path should be returned.
*/
getParentPath: function(path)
{
if (path != null)
{
var index = path.lastIndexOf(mxCellPath.PATH_SEPARATOR);
if (index >= 0)
{
return path.substring(0, index);
}
else if (path.length > 0)
{
return '';
}
}
return null;
},
/**
* Function: resolve
*
* Returns the cell for the specified cell path using the given root as the
* root of the path.
*
* Parameters:
*
* root - Root cell of the path to be resolved.
* path - String that defines the path.
*/
resolve: function(root, path)
{
var parent = root;
if (path != null)
{
var tokens = path.split(mxCellPath.PATH_SEPARATOR);
for (var i=0; i<tokens.length; i++)
{
parent = parent.getChildAt(parseInt(tokens[i]));
}
}
return parent;
},
/**
* Function: compare
*
* Compares the given cell paths and returns -1 if p1 is smaller, 0 if
* p1 is equal and 1 if p1 is greater than p2.
*/
compare: function(p1, p2)
{
var min = Math.min(p1.length, p2.length);
var comp = 0;
for (var i = 0; i < min; i++)
{
if (p1[i] != p2[i])
{
if (p1[i].length == 0 ||
p2[i].length == 0)
{
comp = (p1[i] == p2[i]) ? 0 : ((p1[i] > p2[i]) ? 1 : -1);
}
else
{
var t1 = parseInt(p1[i]);
var t2 = parseInt(p2[i]);
comp = (t1 == t2) ? 0 : ((t1 > t2) ? 1 : -1);
}
break;
}
}
// Compares path length if both paths are equal to this point
if (comp == 0)
{
var t1 = p1.length;
var t2 = p2.length;
if (t1 != t2)
{
comp = (t1 > t2) ? 1 : -1;
}
}
return comp;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxPerimeter =
{
/**
* Class: mxPerimeter
*
* Provides various perimeter functions to be used in a style
* as the value of <mxConstants.STYLE_PERIMETER>. Perimeters for
* rectangle, circle, rhombus and triangle are available.
*
* Example:
*
* (code)
* <add as="perimeter">mxPerimeter.RectanglePerimeter</add>
* (end)
*
* Or programmatically:
*
* (code)
* style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter;
* (end)
*
* When adding new perimeter functions, it is recommended to use the
* mxPerimeter-namespace as follows:
*
* (code)
* mxPerimeter.CustomPerimeter = function (bounds, vertex, next, orthogonal)
* {
* var x = 0; // Calculate x-coordinate
* var y = 0; // Calculate y-coordainte
*
* return new mxPoint(x, y);
* }
* (end)
*
* The new perimeter should then be registered in the <mxStyleRegistry> as follows:
* (code)
* mxStyleRegistry.putValue('customPerimeter', mxPerimeter.CustomPerimeter);
* (end)
*
* The custom perimeter above can now be used in a specific vertex as follows:
*
* (code)
* model.setStyle(vertex, 'perimeter=customPerimeter');
* (end)
*
* Note that the key of the <mxStyleRegistry> entry for the function should
* be used in string values, unless <mxGraphView.allowEval> is true, in
* which case you can also use mxPerimeter.CustomPerimeter for the value in
* the cell style above.
*
* Or it can be used for all vertices in the graph as follows:
*
* (code)
* var style = graph.getStylesheet().getDefaultVertexStyle();
* style[mxConstants.STYLE_PERIMETER] = mxPerimeter.CustomPerimeter;
* (end)
*
* Note that the object can be used directly when programmatically setting
* the value, but the key in the <mxStyleRegistry> should be used when
* setting the value via a key, value pair in a cell style.
*
* The parameters are explained in <RectanglePerimeter>.
*
* Function: RectanglePerimeter
*
* Describes a rectangular perimeter for the given bounds.
*
* Parameters:
*
* bounds - <mxRectangle> that represents the absolute bounds of the
* vertex.
* vertex - <mxCellState> that represents the vertex.
* next - <mxPoint> that represents the nearest neighbour point on the
* given edge.
* orthogonal - Boolean that specifies if the orthogonal projection onto
* the perimeter should be returned. If this is false then the intersection
* of the perimeter and the line between the next and the center point is
* returned.
*/
RectanglePerimeter: function (bounds, vertex, next, orthogonal)
{
var cx = bounds.getCenterX();
var cy = bounds.getCenterY();
var dx = next.x - cx;
var dy = next.y - cy;
var alpha = Math.atan2(dy, dx);
var p = new mxPoint(0, 0);
var pi = Math.PI;
var pi2 = Math.PI/2;
var beta = pi2 - alpha;
var t = Math.atan2(bounds.height, bounds.width);
if (alpha < -pi + t || alpha > pi - t)
{
// Left edge
p.x = bounds.x;
p.y = cy - bounds.width * Math.tan(alpha) / 2;
}
else if (alpha < -t)
{
// Top Edge
p.y = bounds.y;
p.x = cx - bounds.height * Math.tan(beta) / 2;
}
else if (alpha < t)
{
// Right Edge
p.x = bounds.x + bounds.width;
p.y = cy + bounds.width * Math.tan(alpha) / 2;
}
else
{
// Bottom Edge
p.y = bounds.y + bounds.height;
p.x = cx + bounds.height * Math.tan(beta) / 2;
}
if (orthogonal)
{
if (next.x >= bounds.x &&
next.x <= bounds.x + bounds.width)
{
p.x = next.x;
}
else if (next.y >= bounds.y &&
next.y <= bounds.y + bounds.height)
{
p.y = next.y;
}
if (next.x < bounds.x)
{
p.x = bounds.x;
}
else if (next.x > bounds.x + bounds.width)
{
p.x = bounds.x + bounds.width;
}
if (next.y < bounds.y)
{
p.y = bounds.y;
}
else if (next.y > bounds.y + bounds.height)
{
p.y = bounds.y + bounds.height;
}
}
return p;
},
/**
* Function: EllipsePerimeter
*
* Describes an elliptic perimeter. See <RectanglePerimeter>
* for a description of the parameters.
*/
EllipsePerimeter: function (bounds, vertex, next, orthogonal)
{
var x = bounds.x;
var y = bounds.y;
var a = bounds.width / 2;
var b = bounds.height / 2;
var cx = x + a;
var cy = y + b;
var px = next.x;
var py = next.y;
// Calculates straight line equation through
// point and ellipse center y = d * x + h
var dx = parseInt(px - cx);
var dy = parseInt(py - cy);
if (dx == 0 && dy != 0)
{
return new mxPoint(cx, cy + b * dy / Math.abs(dy));
}
else if (dx == 0 && dy == 0)
{
return new mxPoint(px, py);
}
if (orthogonal)
{
if (py >= y && py <= y + bounds.height)
{
var ty = py - cy;
var tx = Math.sqrt(a*a*(1-(ty*ty)/(b*b))) || 0;
if (px <= x)
{
tx = -tx;
}
return new mxPoint(cx+tx, py);
}
if (px >= x && px <= x + bounds.width)
{
var tx = px - cx;
var ty = Math.sqrt(b*b*(1-(tx*tx)/(a*a))) || 0;
if (py <= y)
{
ty = -ty;
}
return new mxPoint(px, cy+ty);
}
}
// Calculates intersection
var d = dy / dx;
var h = cy - d * cx;
var e = a * a * d * d + b * b;
var f = -2 * cx * e;
var g = a * a * d * d * cx * cx +
b * b * cx * cx -
a * a * b * b;
var det = Math.sqrt(f * f - 4 * e * g);
// Two solutions (perimeter points)
var xout1 = (-f + det) / (2 * e);
var xout2 = (-f - det) / (2 * e);
var yout1 = d * xout1 + h;
var yout2 = d * xout2 + h;
var dist1 = Math.sqrt(Math.pow((xout1 - px), 2)
+ Math.pow((yout1 - py), 2));
var dist2 = Math.sqrt(Math.pow((xout2 - px), 2)
+ Math.pow((yout2 - py), 2));
// Correct solution
var xout = 0;
var yout = 0;
if (dist1 < dist2)
{
xout = xout1;
yout = yout1;
}
else
{
xout = xout2;
yout = yout2;
}
return new mxPoint(xout, yout);
},
/**
* Function: RhombusPerimeter
*
* Describes a rhombus (aka diamond) perimeter. See <RectanglePerimeter>
* for a description of the parameters.
*/
RhombusPerimeter: function (bounds, vertex, next, orthogonal)
{
var x = bounds.x;
var y = bounds.y;
var w = bounds.width;
var h = bounds.height;
var cx = x + w / 2;
var cy = y + h / 2;
var px = next.x;
var py = next.y;
// Special case for intersecting the diamond's corners
if (cx == px)
{
if (cy > py)
{
return new mxPoint(cx, y); // top
}
else
{
return new mxPoint(cx, y + h); // bottom
}
}
else if (cy == py)
{
if (cx > px)
{
return new mxPoint(x, cy); // left
}
else
{
return new mxPoint(x + w, cy); // right
}
}
var tx = cx;
var ty = cy;
if (orthogonal)
{
if (px >= x && px <= x + w)
{
tx = px;
}
else if (py >= y && py <= y + h)
{
ty = py;
}
}
// In which quadrant will the intersection be?
// set the slope and offset of the border line accordingly
if (px < cx)
{
if (py < cy)
{
return mxUtils.intersection(px, py, tx, ty, cx, y, x, cy);
}
else
{
return mxUtils.intersection(px, py, tx, ty, cx, y + h, x, cy);
}
}
else if (py < cy)
{
return mxUtils.intersection(px, py, tx, ty, cx, y, x + w, cy);
}
else
{
return mxUtils.intersection(px, py, tx, ty, cx, y + h, x + w, cy);
}
},
/**
* Function: TrianglePerimeter
*
* Describes a triangle perimeter. See <RectanglePerimeter>
* for a description of the parameters.
*/
TrianglePerimeter: function (bounds, vertex, next, orthogonal)
{
var direction = (vertex != null) ?
vertex.style[mxConstants.STYLE_DIRECTION] : null;
var vertical = direction == mxConstants.DIRECTION_NORTH ||
direction == mxConstants.DIRECTION_SOUTH;
var x = bounds.x;
var y = bounds.y;
var w = bounds.width;
var h = bounds.height;
var cx = x + w / 2;
var cy = y + h / 2;
var start = new mxPoint(x, y);
var corner = new mxPoint(x + w, cy);
var end = new mxPoint(x, y + h);
if (direction == mxConstants.DIRECTION_NORTH)
{
start = end;
corner = new mxPoint(cx, y);
end = new mxPoint(x + w, y + h);
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
corner = new mxPoint(cx, y + h);
end = new mxPoint(x + w, y);
}
else if (direction == mxConstants.DIRECTION_WEST)
{
start = new mxPoint(x + w, y);
corner = new mxPoint(x, cy);
end = new mxPoint(x + w, y + h);
}
var dx = next.x - cx;
var dy = next.y - cy;
var alpha = (vertical) ? Math.atan2(dx, dy) : Math.atan2(dy, dx);
var t = (vertical) ? Math.atan2(w, h) : Math.atan2(h, w);
var base = false;
if (direction == mxConstants.DIRECTION_NORTH ||
direction == mxConstants.DIRECTION_WEST)
{
base = alpha > -t && alpha < t;
}
else
{
base = alpha < -Math.PI + t || alpha > Math.PI - t;
}
var result = null;
if (base)
{
if (orthogonal && ((vertical && next.x >= start.x && next.x <= end.x) ||
(!vertical && next.y >= start.y && next.y <= end.y)))
{
if (vertical)
{
result = new mxPoint(next.x, start.y);
}
else
{
result = new mxPoint(start.x, next.y);
}
}
else
{
if (direction == mxConstants.DIRECTION_NORTH)
{
result = new mxPoint(x + w / 2 + h * Math.tan(alpha) / 2,
y + h);
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
result = new mxPoint(x + w / 2 - h * Math.tan(alpha) / 2,
y);
}
else if (direction == mxConstants.DIRECTION_WEST)
{
result = new mxPoint(x + w, y + h / 2 +
w * Math.tan(alpha) / 2);
}
else
{
result = new mxPoint(x, y + h / 2 -
w * Math.tan(alpha) / 2);
}
}
}
else
{
if (orthogonal)
{
var pt = new mxPoint(cx, cy);
if (next.y >= y && next.y <= y + h)
{
pt.x = (vertical) ? cx : (
(direction == mxConstants.DIRECTION_WEST) ?
x + w : x);
pt.y = next.y;
}
else if (next.x >= x && next.x <= x + w)
{
pt.x = next.x;
pt.y = (!vertical) ? cy : (
(direction == mxConstants.DIRECTION_NORTH) ?
y + h : y);
}
// Compute angle
dx = next.x - pt.x;
dy = next.y - pt.y;
cx = pt.x;
cy = pt.y;
}
if ((vertical && next.x <= x + w / 2) ||
(!vertical && next.y <= y + h / 2))
{
result = mxUtils.intersection(next.x, next.y, cx, cy,
start.x, start.y, corner.x, corner.y);
}
else
{
result = mxUtils.intersection(next.x, next.y, cx, cy,
corner.x, corner.y, end.x, end.y);
}
}
if (result == null)
{
result = new mxPoint(cx, cy);
}
return result;
},
/**
* Function: HexagonPerimeter
*
* Describes a hexagon perimeter. See <RectanglePerimeter>
* for a description of the parameters.
*/
HexagonPerimeter: function (bounds, vertex, next, orthogonal)
{
var x = bounds.x;
var y = bounds.y;
var w = bounds.width;
var h = bounds.height;
var cx = bounds.getCenterX();
var cy = bounds.getCenterY();
var px = next.x;
var py = next.y;
var dx = px - cx;
var dy = py - cy;
var alpha = -Math.atan2(dy, dx);
var pi = Math.PI;
var pi2 = Math.PI / 2;
var result = new mxPoint(cx, cy);
var direction = (vertex != null) ? mxUtils.getValue(
vertex.style, mxConstants.STYLE_DIRECTION,
mxConstants.DIRECTION_EAST) : mxConstants.DIRECTION_EAST;
var vertical = direction == mxConstants.DIRECTION_NORTH
|| direction == mxConstants.DIRECTION_SOUTH;
var a = new mxPoint();
var b = new mxPoint();
//Only consider corrects quadrants for the orthogonal case.
if ((px < x) && (py < y) || (px < x) && (py > y + h)
|| (px > x + w) && (py < y) || (px > x + w) && (py > y + h))
{
orthogonal = false;
}
if (orthogonal)
{
if (vertical)
{
//Special cases where intersects with hexagon corners
if (px == cx)
{
if (py <= y)
{
return new mxPoint(cx, y);
}
else if (py >= y + h)
{
return new mxPoint(cx, y + h);
}
}
else if (px < x)
{
if (py == y + h / 4)
{
return new mxPoint(x, y + h / 4);
}
else if (py == y + 3 * h / 4)
{
return new mxPoint(x, y + 3 * h / 4);
}
}
else if (px > x + w)
{
if (py == y + h / 4)
{
return new mxPoint(x + w, y + h / 4);
}
else if (py == y + 3 * h / 4)
{
return new mxPoint(x + w, y + 3 * h / 4);
}
}
else if (px == x)
{
if (py < cy)
{
return new mxPoint(x, y + h / 4);
}
else if (py > cy)
{
return new mxPoint(x, y + 3 * h / 4);
}
}
else if (px == x + w)
{
if (py < cy)
{
return new mxPoint(x + w, y + h / 4);
}
else if (py > cy)
{
return new mxPoint(x + w, y + 3 * h / 4);
}
}
if (py == y)
{
return new mxPoint(cx, y);
}
else if (py == y + h)
{
return new mxPoint(cx, y + h);
}
if (px < cx)
{
if ((py > y + h / 4) && (py < y + 3 * h / 4))
{
a = new mxPoint(x, y);
b = new mxPoint(x, y + h);
}
else if (py < y + h / 4)
{
a = new mxPoint(x - Math.floor(0.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x + w, y - Math.floor(0.25 * h));
}
else if (py > y + 3 * h / 4)
{
a = new mxPoint(x - Math.floor(0.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x + w, y + Math.floor(1.25 * h));
}
}
else if (px > cx)
{
if ((py > y + h / 4) && (py < y + 3 * h / 4))
{
a = new mxPoint(x + w, y);
b = new mxPoint(x + w, y + h);
}
else if (py < y + h / 4)
{
a = new mxPoint(x, y - Math.floor(0.25 * h));
b = new mxPoint(x + Math.floor(1.5 * w), y
+ Math.floor(0.5 * h));
}
else if (py > y + 3 * h / 4)
{
a = new mxPoint(x + Math.floor(1.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x, y + Math.floor(1.25 * h));
}
}
}
else
{
//Special cases where intersects with hexagon corners
if (py == cy)
{
if (px <= x)
{
return new mxPoint(x, y + h / 2);
}
else if (px >= x + w)
{
return new mxPoint(x + w, y + h / 2);
}
}
else if (py < y)
{
if (px == x + w / 4)
{
return new mxPoint(x + w / 4, y);
}
else if (px == x + 3 * w / 4)
{
return new mxPoint(x + 3 * w / 4, y);
}
}
else if (py > y + h)
{
if (px == x + w / 4)
{
return new mxPoint(x + w / 4, y + h);
}
else if (px == x + 3 * w / 4)
{
return new mxPoint(x + 3 * w / 4, y + h);
}
}
else if (py == y)
{
if (px < cx)
{
return new mxPoint(x + w / 4, y);
}
else if (px > cx)
{
return new mxPoint(x + 3 * w / 4, y);
}
}
else if (py == y + h)
{
if (px < cx)
{
return new mxPoint(x + w / 4, y + h);
}
else if (py > cy)
{
return new mxPoint(x + 3 * w / 4, y + h);
}
}
if (px == x)
{
return new mxPoint(x, cy);
}
else if (px == x + w)
{
return new mxPoint(x + w, cy);
}
if (py < cy)
{
if ((px > x + w / 4) && (px < x + 3 * w / 4))
{
a = new mxPoint(x, y);
b = new mxPoint(x + w, y);
}
else if (px < x + w / 4)
{
a = new mxPoint(x - Math.floor(0.25 * w), y + h);
b = new mxPoint(x + Math.floor(0.5 * w), y
- Math.floor(0.5 * h));
}
else if (px > x + 3 * w / 4)
{
a = new mxPoint(x + Math.floor(0.5 * w), y
- Math.floor(0.5 * h));
b = new mxPoint(x + Math.floor(1.25 * w), y + h);
}
}
else if (py > cy)
{
if ((px > x + w / 4) && (px < x + 3 * w / 4))
{
a = new mxPoint(x, y + h);
b = new mxPoint(x + w, y + h);
}
else if (px < x + w / 4)
{
a = new mxPoint(x - Math.floor(0.25 * w), y);
b = new mxPoint(x + Math.floor(0.5 * w), y
+ Math.floor(1.5 * h));
}
else if (px > x + 3 * w / 4)
{
a = new mxPoint(x + Math.floor(0.5 * w), y
+ Math.floor(1.5 * h));
b = new mxPoint(x + Math.floor(1.25 * w), y);
}
}
}
var tx = cx;
var ty = cy;
if (px >= x && px <= x + w)
{
tx = px;
if (py < cy)
{
ty = y + h;
}
else
{
ty = y;
}
}
else if (py >= y && py <= y + h)
{
ty = py;
if (px < cx)
{
tx = x + w;
}
else
{
tx = x;
}
}
result = mxUtils.intersection(tx, ty, next.x, next.y, a.x, a.y, b.x, b.y);
}
else
{
if (vertical)
{
var beta = Math.atan2(h / 4, w / 2);
//Special cases where intersects with hexagon corners
if (alpha == beta)
{
return new mxPoint(x + w, y + Math.floor(0.25 * h));
}
else if (alpha == pi2)
{
return new mxPoint(x + Math.floor(0.5 * w), y);
}
else if (alpha == (pi - beta))
{
return new mxPoint(x, y + Math.floor(0.25 * h));
}
else if (alpha == -beta)
{
return new mxPoint(x + w, y + Math.floor(0.75 * h));
}
else if (alpha == (-pi2))
{
return new mxPoint(x + Math.floor(0.5 * w), y + h);
}
else if (alpha == (-pi + beta))
{
return new mxPoint(x, y + Math.floor(0.75 * h));
}
if ((alpha < beta) && (alpha > -beta))
{
a = new mxPoint(x + w, y);
b = new mxPoint(x + w, y + h);
}
else if ((alpha > beta) && (alpha < pi2))
{
a = new mxPoint(x, y - Math.floor(0.25 * h));
b = new mxPoint(x + Math.floor(1.5 * w), y
+ Math.floor(0.5 * h));
}
else if ((alpha > pi2) && (alpha < (pi - beta)))
{
a = new mxPoint(x - Math.floor(0.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x + w, y - Math.floor(0.25 * h));
}
else if (((alpha > (pi - beta)) && (alpha <= pi))
|| ((alpha < (-pi + beta)) && (alpha >= -pi)))
{
a = new mxPoint(x, y);
b = new mxPoint(x, y + h);
}
else if ((alpha < -beta) && (alpha > -pi2))
{
a = new mxPoint(x + Math.floor(1.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x, y + Math.floor(1.25 * h));
}
else if ((alpha < -pi2) && (alpha > (-pi + beta)))
{
a = new mxPoint(x - Math.floor(0.5 * w), y
+ Math.floor(0.5 * h));
b = new mxPoint(x + w, y + Math.floor(1.25 * h));
}
}
else
{
var beta = Math.atan2(h / 2, w / 4);
//Special cases where intersects with hexagon corners
if (alpha == beta)
{
return new mxPoint(x + Math.floor(0.75 * w), y);
}
else if (alpha == (pi - beta))
{
return new mxPoint(x + Math.floor(0.25 * w), y);
}
else if ((alpha == pi) || (alpha == -pi))
{
return new mxPoint(x, y + Math.floor(0.5 * h));
}
else if (alpha == 0)
{
return new mxPoint(x + w, y + Math.floor(0.5 * h));
}
else if (alpha == -beta)
{
return new mxPoint(x + Math.floor(0.75 * w), y + h);
}
else if (alpha == (-pi + beta))
{
return new mxPoint(x + Math.floor(0.25 * w), y + h);
}
if ((alpha > 0) && (alpha < beta))
{
a = new mxPoint(x + Math.floor(0.5 * w), y
- Math.floor(0.5 * h));
b = new mxPoint(x + Math.floor(1.25 * w), y + h);
}
else if ((alpha > beta) && (alpha < (pi - beta)))
{
a = new mxPoint(x, y);
b = new mxPoint(x + w, y);
}
else if ((alpha > (pi - beta)) && (alpha < pi))
{
a = new mxPoint(x - Math.floor(0.25 * w), y + h);
b = new mxPoint(x + Math.floor(0.5 * w), y
- Math.floor(0.5 * h));
}
else if ((alpha < 0) && (alpha > -beta))
{
a = new mxPoint(x + Math.floor(0.5 * w), y
+ Math.floor(1.5 * h));
b = new mxPoint(x + Math.floor(1.25 * w), y);
}
else if ((alpha < -beta) && (alpha > (-pi + beta)))
{
a = new mxPoint(x, y + h);
b = new mxPoint(x + w, y + h);
}
else if ((alpha < (-pi + beta)) && (alpha > -pi))
{
a = new mxPoint(x - Math.floor(0.25 * w), y);
b = new mxPoint(x + Math.floor(0.5 * w), y
+ Math.floor(1.5 * h));
}
}
result = mxUtils.intersection(cx, cy, next.x, next.y, a.x, a.y, b.x, b.y);
}
if (result == null)
{
return new mxPoint(cx, cy);
}
return result;
}
};
/**
* Copyright (c) 2006-2019, JGraph Ltd
* Copyright (c) 2006-2017, draw.io AG
*/
/**
* Class: mxPrintPreview
*
* Implements printing of a diagram across multiple pages. The following opens
* a print preview for an existing graph:
*
* (code)
* var preview = new mxPrintPreview(graph);
* preview.open();
* (end)
*
* Use <mxUtils.getScaleForPageCount> as follows in order to print the graph
* across a given number of pages:
*
* (code)
* var pageCount = mxUtils.prompt('Enter page count', '1');
*
* if (pageCount != null)
* {
* var scale = mxUtils.getScaleForPageCount(pageCount, graph);
* var preview = new mxPrintPreview(graph, scale);
* preview.open();
* }
* (end)
*
* Additional pages:
*
* To add additional pages before and after the output, <getCoverPages> and
* <getAppendices> can be used, respectively.
*
* (code)
* var preview = new mxPrintPreview(graph, 1);
*
* preview.getCoverPages = function(w, h)
* {
* return [this.renderPage(w, h, 0, 0, mxUtils.bind(this, function(div)
* {
* div.innerHTML = '<div style="position:relative;margin:4px;">Cover Page</p>'
* }))];
* };
*
* preview.getAppendices = function(w, h)
* {
* return [this.renderPage(w, h, 0, 0, mxUtils.bind(this, function(div)
* {
* div.innerHTML = '<div style="position:relative;margin:4px;">Appendix</p>'
* }))];
* };
*
* preview.open();
* (end)
*
* CSS:
*
* The CSS from the original page is not carried over to the print preview.
* To add CSS to the page, use the css argument in the <open> function or
* override <writeHead> to add the respective link tags as follows:
*
* (code)
* var writeHead = preview.writeHead;
* preview.writeHead = function(doc, css)
* {
* writeHead.apply(this, arguments);
* doc.writeln('<link rel="stylesheet" type="text/css" href="style.css">');
* };
* (end)
*
* Padding:
*
* To add a padding to the page in the preview (but not the print output), use
* the following code:
*
* (code)
* preview.writeHead = function(doc)
* {
* writeHead.apply(this, arguments);
*
* doc.writeln('<style type="text/css">');
* doc.writeln('@media screen {');
* doc.writeln(' body > div { padding-top:30px;padding-left:40px;box-sizing:content-box; }');
* doc.writeln('}');
* doc.writeln('</style>');
* };
* (end)
*
* Headers:
*
* Apart from setting the title argument in the mxPrintPreview constructor you
* can override <renderPage> as follows to add a header to any page:
*
* (code)
* var oldRenderPage = mxPrintPreview.prototype.renderPage;
* mxPrintPreview.prototype.renderPage = function(w, h, x, y, content, pageNumber)
* {
* var div = oldRenderPage.apply(this, arguments);
*
* var header = document.createElement('div');
* header.style.position = 'absolute';
* header.style.top = '0px';
* header.style.width = '100%';
* header.style.textAlign = 'right';
* mxUtils.write(header, 'Your header here');
* div.firstChild.appendChild(header);
*
* return div;
* };
* (end)
*
* The pageNumber argument contains the number of the current page, starting at
* 1. To display a header on the first page only, check pageNumber and add a
* vertical offset in the constructor call for the height of the header.
*
* Page Format:
*
* For landscape printing, use <mxConstants.PAGE_FORMAT_A4_LANDSCAPE> as
* the pageFormat in <mxUtils.getScaleForPageCount> and <mxPrintPreview>.
* Keep in mind that one can not set the defaults for the print dialog
* of the operating system from JavaScript so the user must manually choose
* a page format that matches this setting.
*
* You can try passing the following CSS directive to <open> to set the
* page format in the print dialog to landscape. However, this CSS
* directive seems to be ignored in most major browsers, including IE.
*
* (code)
* @page {
* size: landscape;
* }
* (end)
*
* Note that the print preview behaves differently in IE when used from the
* filesystem or via HTTP so printing should always be tested via HTTP.
*
* If you are using a DOCTYPE in the source page you can override <getDoctype>
* and provide the same DOCTYPE for the print preview if required. Here is
* an example for IE8 standards mode.
*
* (code)
* var preview = new mxPrintPreview(graph);
* preview.getDoctype = function()
* {
* return '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=8" ><![endif]-->';
* };
* preview.open();
* (end)
*
* Constructor: mxPrintPreview
*
* Constructs a new print preview for the given parameters.
*
* Parameters:
*
* graph - <mxGraph> to be previewed.
* scale - Optional scale of the output. Default is 1 / <mxGraph.pageScale>.
* pageFormat - <mxRectangle> that specifies the page format (in pixels).
* border - Border in pixels along each side of every page. Note that the
* actual print function in the browser will add another border for
* printing.
* This should match the page format of the printer. Default uses the
* <mxGraph.pageFormat> of the given graph.
* x0 - Optional left offset of the output. Default is 0.
* y0 - Optional top offset of the output. Default is 0.
* borderColor - Optional color of the page border. Default is no border.
* Note that a border is sometimes useful to highlight the printed page
* border in the print preview of the browser.
* title - Optional string that is used for the window title. Default
* is 'Printer-friendly version'.
* pageSelector - Optional boolean that specifies if the page selector
* should appear in the window with the print preview. Default is true.
*/
function mxPrintPreview(graph, scale, pageFormat, border, x0, y0, borderColor, title, pageSelector)
{
this.graph = graph;
this.scale = (scale != null) ? scale : 1 / graph.pageScale;
this.border = (border != null) ? border : 0;
this.pageFormat = mxRectangle.fromRectangle((pageFormat != null) ? pageFormat : graph.pageFormat);
this.title = (title != null) ? title : 'Printer-friendly version';
this.x0 = (x0 != null) ? x0 : 0;
this.y0 = (y0 != null) ? y0 : 0;
this.borderColor = borderColor;
this.pageSelector = (pageSelector != null) ? pageSelector : true;
};
/**
* Variable: graph
*
* Reference to the <mxGraph> that should be previewed.
*/
mxPrintPreview.prototype.graph = null;
/**
* Variable: pageFormat
*
* Holds the <mxRectangle> that defines the page format.
*/
mxPrintPreview.prototype.pageFormat = null;
/**
* Variable: scale
*
* Holds the scale of the print preview.
*/
mxPrintPreview.prototype.scale = null;
/**
* Variable: border
*
* The border inset around each side of every page in the preview. This is set
* to 0 if autoOrigin is false.
*/
mxPrintPreview.prototype.border = 0;
/**
* Variable: marginTop
*
* The margin at the top of the page (number). Default is 0.
*/
mxPrintPreview.prototype.marginTop = 0;
/**
* Variable: marginBottom
*
* The margin at the bottom of the page (number). Default is 0.
*/
mxPrintPreview.prototype.marginBottom = 0;
/**
* Variable: x0
*
* Holds the horizontal offset of the output.
*/
mxPrintPreview.prototype.x0 = 0;
/**
* Variable: y0
*
* Holds the vertical offset of the output.
*/
mxPrintPreview.prototype.y0 = 0;
/**
* Variable: autoOrigin
*
* Specifies if the origin should be automatically computed based on the top,
* left corner of the actual diagram contents. The required offset will be added
* to <x0> and <y0> in <open>. Default is true.
*/
mxPrintPreview.prototype.autoOrigin = true;
/**
* Variable: printOverlays
*
* Specifies if overlays should be printed. Default is false.
*/
mxPrintPreview.prototype.printOverlays = false;
/**
* Variable: printControls
*
* Specifies if controls (such as folding icons) should be printed. Default is
* false.
*/
mxPrintPreview.prototype.printControls = false;
/**
* Variable: printBackgroundImage
*
* Specifies if the background image should be printed. Default is false.
*/
mxPrintPreview.prototype.printBackgroundImage = false;
/**
* Variable: backgroundColor
*
* Holds the color value for the page background color. Default is #ffffff.
*/
mxPrintPreview.prototype.backgroundColor = '#ffffff';
/**
* Variable: borderColor
*
* Holds the color value for the page border.
*/
mxPrintPreview.prototype.borderColor = null;
/**
* Variable: title
*
* Holds the title of the preview window.
*/
mxPrintPreview.prototype.title = null;
/**
* Variable: pageSelector
*
* Boolean that specifies if the page selector should be
* displayed. Default is true.
*/
mxPrintPreview.prototype.pageSelector = null;
/**
* Variable: wnd
*
* Reference to the preview window.
*/
mxPrintPreview.prototype.wnd = null;
/**
* Variable: targetWindow
*
* Assign any window here to redirect the rendering in <open>.
*/
mxPrintPreview.prototype.targetWindow = null;
/**
* Variable: pageCount
*
* Holds the actual number of pages in the preview.
*/
mxPrintPreview.prototype.pageCount = 0;
/**
* Variable: clipping
*
* Specifies is clipping should be used to avoid creating too many cell states
* in large diagrams. The bounding box of the cells in the original diagram is
* used if this is enabled. Default is true.
*/
mxPrintPreview.prototype.clipping = true;
/**
* Function: getWindow
*
* Returns <wnd>.
*/
mxPrintPreview.prototype.getWindow = function()
{
return this.wnd;
};
/**
* Function: getDocType
*
* Returns the string that should go before the HTML tag in the print preview
* page. This implementation returns an X-UA meta tag for IE5 in quirks mode,
* IE8 in IE8 standards mode and edge in IE9 standards mode.
*/
mxPrintPreview.prototype.getDoctype = function()
{
var dt = '';
if (document.documentMode == 5)
{
dt = '<meta http-equiv="X-UA-Compatible" content="IE=5">';
}
else if (document.documentMode == 8)
{
dt = '<meta http-equiv="X-UA-Compatible" content="IE=8">';
}
else if (document.documentMode > 8)
{
// Comment needed to make standards doctype apply in IE
dt = '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->';
}
return dt;
};
/**
* Function: appendGraph
*
* Adds the given graph to the existing print preview.
*
* Parameters:
*
* css - Optional CSS string to be used in the head section.
* targetWindow - Optional window that should be used for rendering. If
* this is specified then no HEAD tag, CSS and BODY tag will be written.
*/
mxPrintPreview.prototype.appendGraph = function(graph, scale, x0, y0, forcePageBreaks, keepOpen)
{
this.graph = graph;
this.scale = (scale != null) ? scale : 1 / graph.pageScale;
this.x0 = x0;
this.y0 = y0;
this.open(null, null, forcePageBreaks, keepOpen);
};
/**
* Function: open
*
* Shows the print preview window. The window is created here if it does
* not exist.
*
* Parameters:
*
* css - Optional CSS string to be used in the head section.
* targetWindow - Optional window that should be used for rendering. If
* this is specified then no HEAD tag, CSS and BODY tag will be written.
*/
mxPrintPreview.prototype.open = function(css, targetWindow, forcePageBreaks, keepOpen)
{
// Closing the window while the page is being rendered may cause an
// exception in IE. This and any other exceptions are simply ignored.
var previousInitializeOverlay = this.graph.cellRenderer.initializeOverlay;
var div = null;
try
{
// Temporarily overrides the method to redirect rendering of overlays
// to the draw pane so that they are visible in the printout
if (this.printOverlays)
{
this.graph.cellRenderer.initializeOverlay = function(state, overlay)
{
overlay.init(state.view.getDrawPane());
};
}
if (this.printControls)
{
this.graph.cellRenderer.initControl = function(state, control, handleEvents, clickHandler)
{
control.dialect = state.view.graph.dialect;
control.init(state.view.getDrawPane());
};
}
this.wnd = (targetWindow != null) ? targetWindow : this.wnd;
var isNewWindow = false;
if (this.wnd == null)
{
isNewWindow = true;
this.wnd = window.open();
}
var doc = this.wnd.document;
if (isNewWindow)
{
var dt = this.getDoctype();
if (dt != null && dt.length > 0)
{
doc.writeln(dt);
}
if (mxClient.IS_VML)
{
doc.writeln('<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">');
}
else
{
if (document.compatMode === 'CSS1Compat')
{
doc.writeln('<!DOCTYPE html>');
}
doc.writeln('<html>');
}
doc.writeln('<head>');
this.writeHead(doc, css);
doc.writeln('</head>');
doc.writeln('<body class="mxPage">');
}
// Computes the horizontal and vertical page count
var bounds = this.graph.getGraphBounds().clone();
var currentScale = this.graph.getView().getScale();
var sc = currentScale / this.scale;
var tr = this.graph.getView().getTranslate();
// Uses the absolute origin with no offset for all printing
if (!this.autoOrigin)
{
this.x0 -= tr.x * this.scale;
this.y0 -= tr.y * this.scale;
bounds.width += bounds.x;
bounds.height += bounds.y;
bounds.x = 0;
bounds.y = 0;
this.border = 0;
}
// Store the available page area
var availableWidth = this.pageFormat.width - (this.border * 2);
var availableHeight = this.pageFormat.height - (this.border * 2);
// Adds margins to page format
this.pageFormat.height += this.marginTop + this.marginBottom;
// Compute the unscaled, untranslated bounds to find
// the number of vertical and horizontal pages
bounds.width /= sc;
bounds.height /= sc;
var hpages = Math.max(1, Math.ceil((bounds.width + this.x0) / availableWidth));
var vpages = Math.max(1, Math.ceil((bounds.height + this.y0) / availableHeight));
this.pageCount = hpages * vpages;
var writePageSelector = mxUtils.bind(this, function()
{
if (this.pageSelector && (vpages > 1 || hpages > 1))
{
var table = this.createPageSelector(vpages, hpages);
doc.body.appendChild(table);
// Implements position: fixed in IE quirks mode
if (mxClient.IS_IE && doc.documentMode == null || doc.documentMode == 5 || doc.documentMode == 8 || doc.documentMode == 7)
{
table.style.position = 'absolute';
var update = function()
{
table.style.top = ((doc.body.scrollTop || doc.documentElement.scrollTop) + 10) + 'px';
};
mxEvent.addListener(this.wnd, 'scroll', function(evt)
{
update();
});
mxEvent.addListener(this.wnd, 'resize', function(evt)
{
update();
});
}
}
});
var addPage = mxUtils.bind(this, function(div, addBreak)
{
// Border of the DIV (aka page) inside the document
if (this.borderColor != null)
{
div.style.borderColor = this.borderColor;
div.style.borderStyle = 'solid';
div.style.borderWidth = '1px';
}
// Needs to be assigned directly because IE doesn't support
// child selectors, eg. body > div { background: white; }
div.style.background = this.backgroundColor;
if (forcePageBreaks || addBreak)
{
div.style.pageBreakAfter = 'always';
}
// NOTE: We are dealing with cross-window DOM here, which
// is a problem in IE, so we copy the HTML markup instead.
// The underlying problem is that the graph display markup
// creation (in mxShape, mxGraphView) is hardwired to using
// document.createElement and hence we must use this document
// to create the complete page and then copy it over to the
// new window.document. This can be fixed later by using the
// ownerDocument of the container in mxShape and mxGraphView.
if (isNewWindow && (mxClient.IS_IE || document.documentMode >= 11 || mxClient.IS_EDGE))
{
// For some obscure reason, removing the DIV from the
// parent before fetching its outerHTML has missing
// fillcolor properties and fill children, so the div
// must be removed afterwards to keep the fillcolors.
doc.writeln(div.outerHTML);
div.parentNode.removeChild(div);
}
else if (mxClient.IS_IE || document.documentMode >= 11 || mxClient.IS_EDGE)
{
var clone = doc.createElement('div');
clone.innerHTML = div.outerHTML;
clone = clone.getElementsByTagName('div')[0];
doc.body.appendChild(clone);
div.parentNode.removeChild(div);
}
else
{
div.parentNode.removeChild(div);
doc.body.appendChild(div);
}
if (forcePageBreaks || addBreak)
{
this.addPageBreak(doc);
}
});
var cov = this.getCoverPages(this.pageFormat.width, this.pageFormat.height);
if (cov != null)
{
for (var i = 0; i < cov.length; i++)
{
addPage(cov[i], true);
}
}
var apx = this.getAppendices(this.pageFormat.width, this.pageFormat.height);
// Appends each page to the page output for printing, making
// sure there will be a page break after each page (ie. div)
for (var i = 0; i < vpages; i++)
{
var dy = i * availableHeight / this.scale - this.y0 / this.scale +
(bounds.y - tr.y * currentScale) / currentScale;
for (var j = 0; j < hpages; j++)
{
if (this.wnd == null)
{
return null;
}
var dx = j * availableWidth / this.scale - this.x0 / this.scale +
(bounds.x - tr.x * currentScale) / currentScale;
var pageNum = i * hpages + j + 1;
var clip = new mxRectangle(dx, dy, availableWidth, availableHeight);
div = this.renderPage(this.pageFormat.width, this.pageFormat.height, 0, 0, mxUtils.bind(this, function(div)
{
this.addGraphFragment(-dx, -dy, this.scale, pageNum, div, clip);
if (this.printBackgroundImage)
{
this.insertBackgroundImage(div, -dx, -dy);
}
}), pageNum);
// Gives the page a unique ID for later accessing the page
div.setAttribute('id', 'mxPage-'+pageNum);
addPage(div, apx != null || i < vpages - 1 || j < hpages - 1);
}
}
if (apx != null)
{
for (var i = 0; i < apx.length; i++)
{
addPage(apx[i], i < apx.length - 1);
}
}
if (isNewWindow && !keepOpen)
{
this.closeDocument();
writePageSelector();
}
this.wnd.focus();
}
catch (e)
{
// Removes the DIV from the document in case of an error
if (div != null && div.parentNode != null)
{
div.parentNode.removeChild(div);
}
}
finally
{
this.graph.cellRenderer.initializeOverlay = previousInitializeOverlay;
}
return this.wnd;
};
/**
* Function: addPageBreak
*
* Adds a page break to the given document.
*/
mxPrintPreview.prototype.addPageBreak = function(doc)
{
var hr = doc.createElement('hr');
hr.className = 'mxPageBreak';
doc.body.appendChild(hr);
};
/**
* Function: closeDocument
*
* Writes the closing tags for body and page after calling <writePostfix>.
*/
mxPrintPreview.prototype.closeDocument = function()
{
try
{
if (this.wnd != null && this.wnd.document != null)
{
var doc = this.wnd.document;
this.writePostfix(doc);
doc.writeln('</body>');
doc.writeln('</html>');
doc.close();
// Removes all event handlers in the print output
mxEvent.release(doc.body);
}
}
catch (e)
{
// ignore any errors resulting from wnd no longer being available
}
};
/**
* Function: writeHead
*
* Writes the HEAD section into the given document, without the opening
* and closing HEAD tags.
*/
mxPrintPreview.prototype.writeHead = function(doc, css)
{
if (this.title != null)
{
doc.writeln('<title>' + this.title + '</title>');
}
// Adds required namespaces
if (mxClient.IS_VML)
{
doc.writeln('<style type="text/css">v\\:*{behavior:url(#default#VML)}o\\:*{behavior:url(#default#VML)}</style>');
}
// Adds all required stylesheets
mxClient.link('stylesheet', mxClient.basePath + '/css/common.css', doc);
// Removes horizontal rules and page selector from print output
doc.writeln('<style type="text/css">');
doc.writeln('@media print {');
doc.writeln(' * { -webkit-print-color-adjust: exact; }');
doc.writeln(' table.mxPageSelector { display: none; }');
doc.writeln(' hr.mxPageBreak { display: none; }');
doc.writeln('}');
doc.writeln('@media screen {');
// NOTE: position: fixed is not supported in IE, so the page selector
// position (absolute) needs to be updated in IE (see below)
doc.writeln(' table.mxPageSelector { position: fixed; right: 10px; top: 10px;' +
'font-family: Arial; font-size:10pt; border: solid 1px darkgray;' +
'background: white; border-collapse:collapse; }');
doc.writeln(' table.mxPageSelector td { border: solid 1px gray; padding:4px; }');
doc.writeln(' body.mxPage { background: gray; }');
doc.writeln('}');
if (css != null)
{
doc.writeln(css);
}
doc.writeln('</style>');
};
/**
* Function: writePostfix
*
* Called before closing the body of the page. This implementation is empty.
*/
mxPrintPreview.prototype.writePostfix = function(doc)
{
// empty
};
/**
* Function: createPageSelector
*
* Creates the page selector table.
*/
mxPrintPreview.prototype.createPageSelector = function(vpages, hpages)
{
var doc = this.wnd.document;
var table = doc.createElement('table');
table.className = 'mxPageSelector';
table.setAttribute('border', '0');
var tbody = doc.createElement('tbody');
for (var i = 0; i < vpages; i++)
{
var row = doc.createElement('tr');
for (var j = 0; j < hpages; j++)
{
var pageNum = i * hpages + j + 1;
var cell = doc.createElement('td');
var a = doc.createElement('a');
a.setAttribute('href', '#mxPage-' + pageNum);
// Workaround for FF where the anchor is appended to the URL of the original document
if (mxClient.IS_NS && !mxClient.IS_SF && !mxClient.IS_GC)
{
var js = 'var page = document.getElementById(\'mxPage-' + pageNum + '\');page.scrollIntoView(true);event.preventDefault();';
a.setAttribute('onclick', js);
}
mxUtils.write(a, pageNum, doc);
cell.appendChild(a);
row.appendChild(cell);
}
tbody.appendChild(row);
}
table.appendChild(tbody);
return table;
};
/**
* Function: renderPage
*
* Creates a DIV that prints a single page of the given
* graph using the given scale and returns the DIV that
* represents the page.
*
* Parameters:
*
* w - Width of the page in pixels.
* h - Height of the page in pixels.
* dx - Optional horizontal page offset in pixels (used internally).
* dy - Optional vertical page offset in pixels (used internally).
* content - Callback that adds the HTML content to the inner div of a page.
* Takes the inner div as the argument.
* pageNumber - Integer representing the page number.
*/
mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, content, pageNumber)
{
var doc = this.wnd.document;
var div = document.createElement('div');
var arg = null;
try
{
// Workaround for ignored clipping in IE 9 standards
// when printing with page breaks and HTML labels.
if (dx != 0 || dy != 0)
{
div.style.position = 'relative';
div.style.width = w + 'px';
div.style.height = h + 'px';
div.style.pageBreakInside = 'avoid';
var innerDiv = document.createElement('div');
innerDiv.style.position = 'relative';
innerDiv.style.top = this.border + 'px';
innerDiv.style.left = this.border + 'px';
innerDiv.style.width = (w - 2 * this.border) + 'px';
innerDiv.style.height = (h - 2 * this.border) + 'px';
innerDiv.style.overflow = 'hidden';
var viewport = document.createElement('div');
viewport.style.position = 'relative';
viewport.style.marginLeft = dx + 'px';
viewport.style.marginTop = dy + 'px';
// FIXME: IE8 standards output problems
if (doc.documentMode == 8)
{
innerDiv.style.position = 'absolute';
viewport.style.position = 'absolute';
}
if (doc.documentMode == 10)
{
viewport.style.width = '100%';
viewport.style.height = '100%';
}
innerDiv.appendChild(viewport);
div.appendChild(innerDiv);
document.body.appendChild(div);
arg = viewport;
}
// FIXME: IE10/11 too many pages
else
{
div.style.width = w + 'px';
div.style.height = h + 'px';
div.style.overflow = 'hidden';
div.style.pageBreakInside = 'avoid';
// IE8 uses above branch currently
if (doc.documentMode == 8)
{
div.style.position = 'relative';
}
var innerDiv = document.createElement('div');
innerDiv.style.width = (w - 2 * this.border) + 'px';
innerDiv.style.height = (h - 2 * this.border) + 'px';
innerDiv.style.overflow = 'hidden';
if (mxClient.IS_IE && (doc.documentMode == null || doc.documentMode == 5 || doc.documentMode == 8 || doc.documentMode == 7))
{
innerDiv.style.marginTop = this.border + 'px';
innerDiv.style.marginLeft = this.border + 'px';
}
else
{
innerDiv.style.top = this.border + 'px';
innerDiv.style.left = this.border + 'px';
}
if (this.graph.dialect == mxConstants.DIALECT_VML)
{
innerDiv.style.position = 'absolute';
}
div.appendChild(innerDiv);
document.body.appendChild(div);
arg = innerDiv;
}
}
catch (e)
{
div.parentNode.removeChild(div);
div = null;
throw e;
}
content(arg);
return div;
};
/**
* Function: getRoot
*
* Returns the root cell for painting the graph.
*/
mxPrintPreview.prototype.getRoot = function()
{
var root = this.graph.view.currentRoot;
if (root == null)
{
root = this.graph.getModel().getRoot();
}
return root;
};
/**
* Function: addGraphFragment
*
* Adds a graph fragment to the given div.
*
* Parameters:
*
* dx - Horizontal translation for the diagram.
* dy - Vertical translation for the diagram.
* scale - Scale for the diagram.
* pageNumber - Number of the page to be rendered.
* div - Div that contains the output.
* clip - Contains the clipping rectangle as an <mxRectangle>.
*/
mxPrintPreview.prototype.addGraphFragment = function(dx, dy, scale, pageNumber, div, clip)
{
var view = this.graph.getView();
var previousContainer = this.graph.container;
this.graph.container = div;
var canvas = view.getCanvas();
var backgroundPane = view.getBackgroundPane();
var drawPane = view.getDrawPane();
var overlayPane = view.getOverlayPane();
var realScale = scale;
if (this.graph.dialect == mxConstants.DIALECT_SVG)
{
view.createSvg();
// Uses CSS transform for scaling
if (!mxClient.NO_FO)
{
var g = view.getDrawPane().parentNode;
var prev = g.getAttribute('transform');
g.setAttribute('transformOrigin', '0 0');
g.setAttribute('transform', 'scale(' + scale + ',' + scale + ')' +
'translate(' + dx + ',' + dy + ')');
scale = 1;
dx = 0;
dy = 0;
}
}
else if (this.graph.dialect == mxConstants.DIALECT_VML)
{
view.createVml();
}
else
{
view.createHtml();
}
// Disables events on the view
var eventsEnabled = view.isEventsEnabled();
view.setEventsEnabled(false);
// Disables the graph to avoid cursors
var graphEnabled = this.graph.isEnabled();
this.graph.setEnabled(false);
// Resets the translation
var translate = view.getTranslate();
view.translate = new mxPoint(dx, dy);
// Redraws only states that intersect the clip
var redraw = this.graph.cellRenderer.redraw;
var states = view.states;
var s = view.scale;
// Gets the transformed clip for intersection check below
if (this.clipping)
{
var tempClip = new mxRectangle((clip.x + translate.x) * s, (clip.y + translate.y) * s,
clip.width * s / realScale, clip.height * s / realScale);
// Checks clipping rectangle for speedup
// Must create terminal states for edge clipping even if terminal outside of clip
this.graph.cellRenderer.redraw = function(state, force, rendering)
{
if (state != null)
{
// Gets original state from graph to find bounding box
var orig = states.get(state.cell);
if (orig != null)
{
var bbox = view.getBoundingBox(orig, false);
// Stops rendering if outside clip for speedup
if (bbox != null && !mxUtils.intersects(tempClip, bbox))
{
return;
}
}
}
redraw.apply(this, arguments);
};
}
var temp = null;
try
{
// Creates the temporary cell states in the view and
// draws them onto the temporary DOM nodes in the view
var cells = [this.getRoot()];
temp = new mxTemporaryCellStates(view, scale, cells, null, mxUtils.bind(this, function(state)
{
return this.getLinkForCellState(state);
}));
}
finally
{
// Removes overlay pane with selection handles
// controls and icons from the print output
if (mxClient.IS_IE)
{
view.overlayPane.innerHTML = '';
view.canvas.style.overflow = 'hidden';
view.canvas.style.position = 'relative';
view.canvas.style.top = this.marginTop + 'px';
view.canvas.style.width = clip.width + 'px';
view.canvas.style.height = clip.height + 'px';
}
else
{
// Removes everything but the SVG node
var tmp = div.firstChild;
while (tmp != null)
{
var next = tmp.nextSibling;
var name = tmp.nodeName.toLowerCase();
// Note: Width and height are required in FF 11
if (name == 'svg')
{
tmp.style.overflow = 'hidden';
tmp.style.position = 'relative';
tmp.style.top = this.marginTop + 'px';
tmp.setAttribute('width', clip.width);
tmp.setAttribute('height', clip.height);
tmp.style.width = '';
tmp.style.height = '';
}
// Tries to fetch all text labels and only text labels
else if (tmp.style.cursor != 'default' && name != 'div')
{
tmp.parentNode.removeChild(tmp);
}
tmp = next;
}
}
// Puts background image behind SVG output
if (this.printBackgroundImage)
{
var svgs = div.getElementsByTagName('svg');
if (svgs.length > 0)
{
svgs[0].style.position = 'absolute';
}
}
// Completely removes the overlay pane to remove more handles
view.overlayPane.parentNode.removeChild(view.overlayPane);
// Restores the state of the view
this.graph.setEnabled(graphEnabled);
this.graph.container = previousContainer;
this.graph.cellRenderer.redraw = redraw;
view.canvas = canvas;
view.backgroundPane = backgroundPane;
view.drawPane = drawPane;
view.overlayPane = overlayPane;
view.translate = translate;
temp.destroy();
view.setEventsEnabled(eventsEnabled);
}
};
/**
* Function: getLinkForCellState
*
* Returns the link for the given cell state. This returns null.
*/
mxPrintPreview.prototype.getLinkForCellState = function(state)
{
return this.graph.getLinkForCell(state.cell);
};
/**
* Function: insertBackgroundImage
*
* Inserts the background image into the given div.
*/
mxPrintPreview.prototype.insertBackgroundImage = function(div, dx, dy)
{
var bg = this.graph.backgroundImage;
if (bg != null)
{
var img = document.createElement('img');
img.style.position = 'absolute';
img.style.marginLeft = Math.round(dx * this.scale) + 'px';
img.style.marginTop = Math.round(dy * this.scale) + 'px';
img.setAttribute('width', Math.round(this.scale * bg.width));
img.setAttribute('height', Math.round(this.scale * bg.height));
img.src = bg.src;
div.insertBefore(img, div.firstChild);
}
};
/**
* Function: getCoverPages
*
* Returns the pages to be added before the print output. This returns null.
*/
mxPrintPreview.prototype.getCoverPages = function()
{
return null;
};
/**
* Function: getAppendices
*
* Returns the pages to be added after the print output. This returns null.
*/
mxPrintPreview.prototype.getAppendices = function()
{
return null;
};
/**
* Function: print
*
* Opens the print preview and shows the print dialog.
*
* Parameters:
*
* css - Optional CSS string to be used in the head section.
*/
mxPrintPreview.prototype.print = function(css)
{
var wnd = this.open(css);
if (wnd != null)
{
wnd.print();
}
};
/**
* Function: close
*
* Closes the print preview window.
*/
mxPrintPreview.prototype.close = function()
{
if (this.wnd != null)
{
this.wnd.close();
this.wnd = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStylesheet
*
* Defines the appearance of the cells in a graph. See <putCellStyle> for an
* example of creating a new cell style. It is recommended to use objects, not
* arrays for holding cell styles. Existing styles can be cloned using
* <mxUtils.clone> and turned into a string for debugging using
* <mxUtils.toString>.
*
* Default Styles:
*
* The stylesheet contains two built-in styles, which are used if no style is
* defined for a cell:
*
* defaultVertex - Default style for vertices
* defaultEdge - Default style for edges
*
* Example:
*
* (code)
* var vertexStyle = stylesheet.getDefaultVertexStyle();
* vertexStyle[mxConstants.ROUNDED] = true;
* var edgeStyle = stylesheet.getDefaultEdgeStyle();
* edgeStyle[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
* (end)
*
* Modifies the built-in default styles.
*
* To avoid the default style for a cell, add a leading semicolon
* to the style definition, eg.
*
* (code)
* ;shadow=1
* (end)
*
* Removing keys:
*
* For removing a key in a cell style of the form [stylename;|key=value;] the
* special value none can be used, eg. highlight;fillColor=none
*
* See also the helper methods in mxUtils to modify strings of this format,
* namely <mxUtils.setStyle>, <mxUtils.indexOfStylename>,
* <mxUtils.addStylename>, <mxUtils.removeStylename>,
* <mxUtils.removeAllStylenames> and <mxUtils.setStyleFlag>.
*
* Constructor: mxStylesheet
*
* Constructs a new stylesheet and assigns default styles.
*/
function mxStylesheet()
{
this.styles = new Object();
this.putDefaultVertexStyle(this.createDefaultVertexStyle());
this.putDefaultEdgeStyle(this.createDefaultEdgeStyle());
};
/**
* Function: styles
*
* Maps from names to cell styles. Each cell style is a map of key,
* value pairs.
*/
mxStylesheet.prototype.styles;
/**
* Function: createDefaultVertexStyle
*
* Creates and returns the default vertex style.
*/
mxStylesheet.prototype.createDefaultVertexStyle = function()
{
var style = new Object();
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter;
style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE;
style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER;
style[mxConstants.STYLE_FILLCOLOR] = '#C3D9FF';
style[mxConstants.STYLE_STROKECOLOR] = '#6482B9';
style[mxConstants.STYLE_FONTCOLOR] = '#774400';
return style;
};
/**
* Function: createDefaultEdgeStyle
*
* Creates and returns the default edge style.
*/
mxStylesheet.prototype.createDefaultEdgeStyle = function()
{
var style = new Object();
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_CONNECTOR;
style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC;
style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE;
style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER;
style[mxConstants.STYLE_STROKECOLOR] = '#6482B9';
style[mxConstants.STYLE_FONTCOLOR] = '#446299';
return style;
};
/**
* Function: putDefaultVertexStyle
*
* Sets the default style for vertices using defaultVertex as the
* stylename.
*
* Parameters:
* style - Key, value pairs that define the style.
*/
mxStylesheet.prototype.putDefaultVertexStyle = function(style)
{
this.putCellStyle('defaultVertex', style);
};
/**
* Function: putDefaultEdgeStyle
*
* Sets the default style for edges using defaultEdge as the stylename.
*/
mxStylesheet.prototype.putDefaultEdgeStyle = function(style)
{
this.putCellStyle('defaultEdge', style);
};
/**
* Function: getDefaultVertexStyle
*
* Returns the default style for vertices.
*/
mxStylesheet.prototype.getDefaultVertexStyle = function()
{
return this.styles['defaultVertex'];
};
/**
* Function: getDefaultEdgeStyle
*
* Sets the default style for edges.
*/
mxStylesheet.prototype.getDefaultEdgeStyle = function()
{
return this.styles['defaultEdge'];
};
/**
* Function: putCellStyle
*
* Stores the given map of key, value pairs under the given name in
* <styles>.
*
* Example:
*
* The following example adds a new style called 'rounded' into an
* existing stylesheet:
*
* (code)
* var style = new Object();
* style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
* style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter;
* style[mxConstants.STYLE_ROUNDED] = true;
* graph.getStylesheet().putCellStyle('rounded', style);
* (end)
*
* In the above example, the new style is an object. The possible keys of
* the object are all the constants in <mxConstants> that start with STYLE
* and the values are either JavaScript objects, such as
* <mxPerimeter.RightAngleRectanglePerimeter> (which is in fact a function)
* or expressions, such as true. Note that not all keys will be
* interpreted by all shapes (eg. the line shape ignores the fill color).
* The final call to this method associates the style with a name in the
* stylesheet. The style is used in a cell with the following code:
*
* (code)
* model.setStyle(cell, 'rounded');
* (end)
*
* Parameters:
*
* name - Name for the style to be stored.
* style - Key, value pairs that define the style.
*/
mxStylesheet.prototype.putCellStyle = function(name, style)
{
this.styles[name] = style;
};
/**
* Function: getCellStyle
*
* Returns the cell style for the specified stylename or the given
* defaultStyle if no style can be found for the given stylename.
*
* Parameters:
*
* name - String of the form [(stylename|key=value);] that represents the
* style.
* defaultStyle - Default style to be returned if no style can be found.
*/
mxStylesheet.prototype.getCellStyle = function(name, defaultStyle)
{
var style = defaultStyle;
if (name != null && name.length > 0)
{
var pairs = name.split(';');
if (style != null &&
name.charAt(0) != ';')
{
style = mxUtils.clone(style);
}
else
{
style = new Object();
}
// Parses each key, value pair into the existing style
for (var i = 0; i < pairs.length; i++)
{
var tmp = pairs[i];
var pos = tmp.indexOf('=');
if (pos >= 0)
{
var key = tmp.substring(0, pos);
var value = tmp.substring(pos + 1);
if (value == mxConstants.NONE)
{
delete style[key];
}
else if (mxUtils.isNumeric(value))
{
style[key] = parseFloat(value);
}
else
{
style[key] = value;
}
}
else
{
// Merges the entries from a named style
var tmpStyle = this.styles[tmp];
if (tmpStyle != null)
{
for (var key in tmpStyle)
{
style[key] = tmpStyle[key];
}
}
}
}
}
return style;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellState
*
* Represents the current state of a cell in a given <mxGraphView>.
*
* For edges, the edge label position is stored in <absoluteOffset>.
*
* The size for oversize labels can be retrieved using the boundingBox property
* of the <text> field as shown below.
*
* (code)
* var bbox = (state.text != null) ? state.text.boundingBox : null;
* (end)
*
* Constructor: mxCellState
*
* Constructs a new object that represents the current state of the given
* cell in the specified view.
*
* Parameters:
*
* view - <mxGraphView> that contains the state.
* cell - <mxCell> that this state represents.
* style - Array of key, value pairs that constitute the style.
*/
function mxCellState(view, cell, style)
{
this.view = view;
this.cell = cell;
this.style = (style != null) ? style : {};
this.origin = new mxPoint();
this.absoluteOffset = new mxPoint();
};
/**
* Extends mxRectangle.
*/
mxCellState.prototype = new mxRectangle();
mxCellState.prototype.constructor = mxCellState;
/**
* Variable: view
*
* Reference to the enclosing <mxGraphView>.
*/
mxCellState.prototype.view = null;
/**
* Variable: cell
*
* Reference to the <mxCell> that is represented by this state.
*/
mxCellState.prototype.cell = null;
/**
* Variable: style
*
* Contains an array of key, value pairs that represent the style of the
* cell.
*/
mxCellState.prototype.style = null;
/**
* Variable: invalidStyle
*
* Specifies if the style is invalid. Default is false.
*/
mxCellState.prototype.invalidStyle = false;
/**
* Variable: invalid
*
* Specifies if the state is invalid. Default is true.
*/
mxCellState.prototype.invalid = true;
/**
* Variable: origin
*
* <mxPoint> that holds the origin for all child cells. Default is a new
* empty <mxPoint>.
*/
mxCellState.prototype.origin = null;
/**
* Variable: absolutePoints
*
* Holds an array of <mxPoints> that represent the absolute points of an
* edge.
*/
mxCellState.prototype.absolutePoints = null;
/**
* Variable: absoluteOffset
*
* <mxPoint> that holds the absolute offset. For edges, this is the
* absolute coordinates of the label position. For vertices, this is the
* offset of the label relative to the top, left corner of the vertex.
*/
mxCellState.prototype.absoluteOffset = null;
/**
* Variable: visibleSourceState
*
* Caches the visible source terminal state.
*/
mxCellState.prototype.visibleSourceState = null;
/**
* Variable: visibleTargetState
*
* Caches the visible target terminal state.
*/
mxCellState.prototype.visibleTargetState = null;
/**
* Variable: terminalDistance
*
* Caches the distance between the end points for an edge.
*/
mxCellState.prototype.terminalDistance = 0;
/**
* Variable: length
*
* Caches the length of an edge.
*/
mxCellState.prototype.length = 0;
/**
* Variable: segments
*
* Array of numbers that represent the cached length of each segment of the
* edge.
*/
mxCellState.prototype.segments = null;
/**
* Variable: shape
*
* Holds the <mxShape> that represents the cell graphically.
*/
mxCellState.prototype.shape = null;
/**
* Variable: text
*
* Holds the <mxText> that represents the label of the cell. Thi smay be
* null if the cell has no label.
*/
mxCellState.prototype.text = null;
/**
* Variable: unscaledWidth
*
* Holds the unscaled width of the state.
*/
mxCellState.prototype.unscaledWidth = null;
/**
* Function: getPerimeterBounds
*
* Returns the <mxRectangle> that should be used as the perimeter of the
* cell.
*
* Parameters:
*
* border - Optional border to be added around the perimeter bounds.
* bounds - Optional <mxRectangle> to be used as the initial bounds.
*/
mxCellState.prototype.getPerimeterBounds = function(border, bounds)
{
border = border || 0;
bounds = (bounds != null) ? bounds : new mxRectangle(this.x, this.y, this.width, this.height);
if (this.shape != null && this.shape.stencil != null && this.shape.stencil.aspect == 'fixed')
{
var aspect = this.shape.stencil.computeAspect(this.style, bounds.x, bounds.y, bounds.width, bounds.height);
bounds.x = aspect.x;
bounds.y = aspect.y;
bounds.width = this.shape.stencil.w0 * aspect.width;
bounds.height = this.shape.stencil.h0 * aspect.height;
}
if (border != 0)
{
bounds.grow(border);
}
return bounds;
};
/**
* Function: setAbsoluteTerminalPoint
*
* Sets the first or last point in <absolutePoints> depending on isSource.
*
* Parameters:
*
* point - <mxPoint> that represents the terminal point.
* isSource - Boolean that specifies if the first or last point should
* be assigned.
*/
mxCellState.prototype.setAbsoluteTerminalPoint = function(point, isSource)
{
if (isSource)
{
if (this.absolutePoints == null)
{
this.absolutePoints = [];
}
if (this.absolutePoints.length == 0)
{
this.absolutePoints.push(point);
}
else
{
this.absolutePoints[0] = point;
}
}
else
{
if (this.absolutePoints == null)
{
this.absolutePoints = [];
this.absolutePoints.push(null);
this.absolutePoints.push(point);
}
else if (this.absolutePoints.length == 1)
{
this.absolutePoints.push(point);
}
else
{
this.absolutePoints[this.absolutePoints.length - 1] = point;
}
}
};
/**
* Function: setCursor
*
* Sets the given cursor on the shape and text shape.
*/
mxCellState.prototype.setCursor = function(cursor)
{
if (this.shape != null)
{
this.shape.setCursor(cursor);
}
if (this.text != null)
{
this.text.setCursor(cursor);
}
};
/**
* Function: getVisibleTerminal
*
* Returns the visible source or target terminal cell.
*
* Parameters:
*
* source - Boolean that specifies if the source or target cell should be
* returned.
*/
mxCellState.prototype.getVisibleTerminal = function(source)
{
var tmp = this.getVisibleTerminalState(source);
return (tmp != null) ? tmp.cell : null;
};
/**
* Function: getVisibleTerminalState
*
* Returns the visible source or target terminal state.
*
* Parameters:
*
* source - Boolean that specifies if the source or target state should be
* returned.
*/
mxCellState.prototype.getVisibleTerminalState = function(source)
{
return (source) ? this.visibleSourceState : this.visibleTargetState;
};
/**
* Function: setVisibleTerminalState
*
* Sets the visible source or target terminal state.
*
* Parameters:
*
* terminalState - <mxCellState> that represents the terminal.
* source - Boolean that specifies if the source or target state should be set.
*/
mxCellState.prototype.setVisibleTerminalState = function(terminalState, source)
{
if (source)
{
this.visibleSourceState = terminalState;
}
else
{
this.visibleTargetState = terminalState;
}
};
/**
* Function: getCellBounds
*
* Returns the unscaled, untranslated bounds.
*/
mxCellState.prototype.getCellBounds = function()
{
return this.cellBounds;
};
/**
* Function: getPaintBounds
*
* Returns the unscaled, untranslated paint bounds. This is the same as
* <getCellBounds> but with a 90 degree rotation if the shape's
* isPaintBoundsInverted returns true.
*/
mxCellState.prototype.getPaintBounds = function()
{
return this.paintBounds;
};
/**
* Function: updateCachedBounds
*
* Updates the cellBounds and paintBounds.
*/
mxCellState.prototype.updateCachedBounds = function()
{
var tr = this.view.translate;
var s = this.view.scale;
this.cellBounds = new mxRectangle(this.x / s - tr.x, this.y / s - tr.y, this.width / s, this.height / s);
this.paintBounds = mxRectangle.fromRectangle(this.cellBounds);
if (this.shape != null && this.shape.isPaintBoundsInverted())
{
this.paintBounds.rotate90();
}
};
/**
* Destructor: setState
*
* Copies all fields from the given state to this state.
*/
mxCellState.prototype.setState = function(state)
{
this.view = state.view;
this.cell = state.cell;
this.style = state.style;
this.absolutePoints = state.absolutePoints;
this.origin = state.origin;
this.absoluteOffset = state.absoluteOffset;
this.boundingBox = state.boundingBox;
this.terminalDistance = state.terminalDistance;
this.segments = state.segments;
this.length = state.length;
this.x = state.x;
this.y = state.y;
this.width = state.width;
this.height = state.height;
this.unscaledWidth = state.unscaledWidth;
};
/**
* Function: clone
*
* Returns a clone of this <mxPoint>.
*/
mxCellState.prototype.clone = function()
{
var clone = new mxCellState(this.view, this.cell, this.style);
// Clones the absolute points
if (this.absolutePoints != null)
{
clone.absolutePoints = [];
for (var i = 0; i < this.absolutePoints.length; i++)
{
clone.absolutePoints[i] = this.absolutePoints[i].clone();
}
}
if (this.origin != null)
{
clone.origin = this.origin.clone();
}
if (this.absoluteOffset != null)
{
clone.absoluteOffset = this.absoluteOffset.clone();
}
if (this.boundingBox != null)
{
clone.boundingBox = this.boundingBox.clone();
}
clone.terminalDistance = this.terminalDistance;
clone.segments = this.segments;
clone.length = this.length;
clone.x = this.x;
clone.y = this.y;
clone.width = this.width;
clone.height = this.height;
clone.unscaledWidth = this.unscaledWidth;
return clone;
};
/**
* Destructor: destroy
*
* Destroys the state and all associated resources.
*/
mxCellState.prototype.destroy = function()
{
this.view.graph.cellRenderer.destroy(this);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphSelectionModel
*
* Implements the selection model for a graph. Here is a listener that handles
* all removed selection cells.
*
* (code)
* graph.getSelectionModel().addListener(mxEvent.CHANGE, function(sender, evt)
* {
* var cells = evt.getProperty('added');
*
* for (var i = 0; i < cells.length; i++)
* {
* // Handle cells[i]...
* }
* });
* (end)
*
* Event: mxEvent.UNDO
*
* Fires after the selection was changed in <changeSelection>. The
* <code>edit</code> property contains the <mxUndoableEdit> which contains the
* <mxSelectionChange>.
*
* Event: mxEvent.CHANGE
*
* Fires after the selection changes by executing an <mxSelectionChange>. The
* <code>added</code> and <code>removed</code> properties contain arrays of
* cells that have been added to or removed from the selection, respectively.
* The names are inverted due to historic reasons. This cannot be changed.
*
* Constructor: mxGraphSelectionModel
*
* Constructs a new graph selection model for the given <mxGraph>.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxGraphSelectionModel(graph)
{
this.graph = graph;
this.cells = [];
};
/**
* Extends mxEventSource.
*/
mxGraphSelectionModel.prototype = new mxEventSource();
mxGraphSelectionModel.prototype.constructor = mxGraphSelectionModel;
/**
* Variable: doneResource
*
* Specifies the resource key for the status message after a long operation.
* If the resource for this key does not exist then the value is used as
* the status message. Default is 'done'.
*/
mxGraphSelectionModel.prototype.doneResource = (mxClient.language != 'none') ? 'done' : '';
/**
* Variable: updatingSelectionResource
*
* Specifies the resource key for the status message while the selection is
* being updated. If the resource for this key does not exist then the
* value is used as the status message. Default is 'updatingSelection'.
*/
mxGraphSelectionModel.prototype.updatingSelectionResource = (mxClient.language != 'none') ? 'updatingSelection' : '';
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxGraphSelectionModel.prototype.graph = null;
/**
* Variable: singleSelection
*
* Specifies if only one selected item at a time is allowed.
* Default is false.
*/
mxGraphSelectionModel.prototype.singleSelection = false;
/**
* Function: isSingleSelection
*
* Returns <singleSelection> as a boolean.
*/
mxGraphSelectionModel.prototype.isSingleSelection = function()
{
return this.singleSelection;
};
/**
* Function: setSingleSelection
*
* Sets the <singleSelection> flag.
*
* Parameters:
*
* singleSelection - Boolean that specifies the new value for
* <singleSelection>.
*/
mxGraphSelectionModel.prototype.setSingleSelection = function(singleSelection)
{
this.singleSelection = singleSelection;
};
/**
* Function: isSelected
*
* Returns true if the given <mxCell> is selected.
*/
mxGraphSelectionModel.prototype.isSelected = function(cell)
{
if (cell != null)
{
return mxUtils.indexOf(this.cells, cell) >= 0;
}
return false;
};
/**
* Function: isEmpty
*
* Returns true if no cells are currently selected.
*/
mxGraphSelectionModel.prototype.isEmpty = function()
{
return this.cells.length == 0;
};
/**
* Function: clear
*
* Clears the selection and fires a <change> event if the selection was not
* empty.
*/
mxGraphSelectionModel.prototype.clear = function()
{
this.changeSelection(null, this.cells);
};
/**
* Function: setCell
*
* Selects the specified <mxCell> using <setCells>.
*
* Parameters:
*
* cell - <mxCell> to be selected.
*/
mxGraphSelectionModel.prototype.setCell = function(cell)
{
if (cell != null)
{
this.setCells([cell]);
}
};
/**
* Function: setCells
*
* Selects the given array of <mxCells> and fires a <change> event.
*
* Parameters:
*
* cells - Array of <mxCells> to be selected.
*/
mxGraphSelectionModel.prototype.setCells = function(cells)
{
if (cells != null)
{
if (this.singleSelection)
{
cells = [this.getFirstSelectableCell(cells)];
}
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
if (this.graph.isCellSelectable(cells[i]))
{
tmp.push(cells[i]);
}
}
this.changeSelection(tmp, this.cells);
}
};
/**
* Function: getFirstSelectableCell
*
* Returns the first selectable cell in the given array of cells.
*/
mxGraphSelectionModel.prototype.getFirstSelectableCell = function(cells)
{
if (cells != null)
{
for (var i = 0; i < cells.length; i++)
{
if (this.graph.isCellSelectable(cells[i]))
{
return cells[i];
}
}
}
return null;
};
/**
* Function: addCell
*
* Adds the given <mxCell> to the selection and fires a <select> event.
*
* Parameters:
*
* cell - <mxCell> to add to the selection.
*/
mxGraphSelectionModel.prototype.addCell = function(cell)
{
if (cell != null)
{
this.addCells([cell]);
}
};
/**
* Function: addCells
*
* Adds the given array of <mxCells> to the selection and fires a <select>
* event.
*
* Parameters:
*
* cells - Array of <mxCells> to add to the selection.
*/
mxGraphSelectionModel.prototype.addCells = function(cells)
{
if (cells != null)
{
var remove = null;
if (this.singleSelection)
{
remove = this.cells;
cells = [this.getFirstSelectableCell(cells)];
}
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
if (!this.isSelected(cells[i]) &&
this.graph.isCellSelectable(cells[i]))
{
tmp.push(cells[i]);
}
}
this.changeSelection(tmp, remove);
}
};
/**
* Function: removeCell
*
* Removes the specified <mxCell> from the selection and fires a <select>
* event for the remaining cells.
*
* Parameters:
*
* cell - <mxCell> to remove from the selection.
*/
mxGraphSelectionModel.prototype.removeCell = function(cell)
{
if (cell != null)
{
this.removeCells([cell]);
}
};
/**
* Function: removeCells
*/
mxGraphSelectionModel.prototype.removeCells = function(cells)
{
if (cells != null)
{
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
if (this.isSelected(cells[i]))
{
tmp.push(cells[i]);
}
}
this.changeSelection(null, tmp);
}
};
/**
* Function: changeSelection
*
* Adds/removes the specified arrays of <mxCell> to/from the selection.
*
* Parameters:
*
* added - Array of <mxCell> to add to the selection.
* remove - Array of <mxCell> to remove from the selection.
*/
mxGraphSelectionModel.prototype.changeSelection = function(added, removed)
{
if ((added != null &&
added.length > 0 &&
added[0] != null) ||
(removed != null &&
removed.length > 0 &&
removed[0] != null))
{
var change = new mxSelectionChange(this, added, removed);
change.execute();
var edit = new mxUndoableEdit(this, false);
edit.add(change);
this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
}
};
/**
* Function: cellAdded
*
* Inner callback to add the specified <mxCell> to the selection. No event
* is fired in this implementation.
*
* Paramters:
*
* cell - <mxCell> to add to the selection.
*/
mxGraphSelectionModel.prototype.cellAdded = function(cell)
{
if (cell != null &&
!this.isSelected(cell))
{
this.cells.push(cell);
}
};
/**
* Function: cellRemoved
*
* Inner callback to remove the specified <mxCell> from the selection. No
* event is fired in this implementation.
*
* Parameters:
*
* cell - <mxCell> to remove from the selection.
*/
mxGraphSelectionModel.prototype.cellRemoved = function(cell)
{
if (cell != null)
{
var index = mxUtils.indexOf(this.cells, cell);
if (index >= 0)
{
this.cells.splice(index, 1);
}
}
};
/**
* Class: mxSelectionChange
*
* Action to change the current root in a view.
*
* Constructor: mxCurrentRootChange
*
* Constructs a change of the current root in the given view.
*/
function mxSelectionChange(selectionModel, added, removed)
{
this.selectionModel = selectionModel;
this.added = (added != null) ? added.slice() : null;
this.removed = (removed != null) ? removed.slice() : null;
};
/**
* Function: execute
*
* Changes the current root of the view.
*/
mxSelectionChange.prototype.execute = function()
{
var t0 = mxLog.enter('mxSelectionChange.execute');
window.status = mxResources.get(
this.selectionModel.updatingSelectionResource) ||
this.selectionModel.updatingSelectionResource;
if (this.removed != null)
{
for (var i = 0; i < this.removed.length; i++)
{
this.selectionModel.cellRemoved(this.removed[i]);
}
}
if (this.added != null)
{
for (var i = 0; i < this.added.length; i++)
{
this.selectionModel.cellAdded(this.added[i]);
}
}
var tmp = this.added;
this.added = this.removed;
this.removed = tmp;
window.status = mxResources.get(this.selectionModel.doneResource) ||
this.selectionModel.doneResource;
mxLog.leave('mxSelectionChange.execute', t0);
this.selectionModel.fireEvent(new mxEventObject(mxEvent.CHANGE,
'added', this.added, 'removed', this.removed));
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellEditor
*
* In-place editor for the graph. To control this editor, use
* <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and
* <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then
* ctrl-enter or shift-enter can be used to create a linefeed. The F2 and
* escape keys can always be used to stop editing.
*
* To customize the location of the textbox in the graph, override
* <getEditorBounds> as follows:
*
* (code)
* graph.cellEditor.getEditorBounds = function(state)
* {
* var result = mxCellEditor.prototype.getEditorBounds.apply(this, arguments);
*
* if (this.graph.getModel().isEdge(state.cell))
* {
* result.x = state.getCenterX() - result.width / 2;
* result.y = state.getCenterY() - result.height / 2;
* }
*
* return result;
* };
* (end)
*
* Note that this hook is only called if <autoSize> is false. If <autoSize> is true,
* then <mxShape.getLabelBounds> is used to compute the current bounds of the textbox.
*
* The textarea uses the mxCellEditor CSS class. You can modify this class in
* your custom CSS. Note: You should modify the CSS after loading the client
* in the page.
*
* Example:
*
* To only allow numeric input in the in-place editor, use the following code.
*
* (code)
* var text = graph.cellEditor.textarea;
*
* mxEvent.addListener(text, 'keydown', function (evt)
* {
* if (!(evt.keyCode >= 48 && evt.keyCode <= 57) &&
* !(evt.keyCode >= 96 && evt.keyCode <= 105))
* {
* mxEvent.consume(evt);
* }
* });
* (end)
*
* Placeholder:
*
* To implement a placeholder for cells without a label, use the
* <emptyLabelText> variable.
*
* Resize in Chrome:
*
* Resize of the textarea is disabled by default. If you want to enable
* this feature extend <init> and set this.textarea.style.resize = ''.
*
* To start editing on a key press event, the container of the graph
* should have focus or a focusable parent should be used to add the
* key press handler as follows.
*
* (code)
* mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt)
* {
* if (!graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
* !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
* {
* graph.startEditing();
*
* if (mxClient.IS_FF)
* {
* graph.cellEditor.textarea.value = String.fromCharCode(evt.which);
* }
* }
* }));
* (end)
*
* To allow focus for a DIV, and hence to receive key press events, some browsers
* require it to have a valid tabindex attribute. In this case the following
* code may be used to keep the container focused.
*
* (code)
* var graphFireMouseEvent = graph.fireMouseEvent;
* graph.fireMouseEvent = function(evtName, me, sender)
* {
* if (evtName == mxEvent.MOUSE_DOWN)
* {
* this.container.focus();
* }
*
* graphFireMouseEvent.apply(this, arguments);
* };
* (end)
*
* Constructor: mxCellEditor
*
* Constructs a new in-place editor for the specified graph.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxCellEditor(graph)
{
this.graph = graph;
// Stops editing after zoom changes
this.zoomHandler = mxUtils.bind(this, function()
{
if (this.graph.isEditing())
{
this.resize();
}
});
this.graph.view.addListener(mxEvent.SCALE, this.zoomHandler);
this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.zoomHandler);
// Adds handling of deleted cells while editing
this.changeHandler = mxUtils.bind(this, function(sender)
{
if (this.editingCell != null && this.graph.getView().getState(this.editingCell) == null)
{
this.stopEditing(true);
}
});
this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellEditor.prototype.graph = null;
/**
* Variable: textarea
*
* Holds the DIV that is used for text editing. Note that this may be null before the first
* edit. Instantiated in <init>.
*/
mxCellEditor.prototype.textarea = null;
/**
* Variable: editingCell
*
* Reference to the <mxCell> that is currently being edited.
*/
mxCellEditor.prototype.editingCell = null;
/**
* Variable: trigger
*
* Reference to the event that was used to start editing.
*/
mxCellEditor.prototype.trigger = null;
/**
* Variable: modified
*
* Specifies if the label has been modified.
*/
mxCellEditor.prototype.modified = false;
/**
* Variable: autoSize
*
* Specifies if the textarea should be resized while the text is being edited.
* Default is true.
*/
mxCellEditor.prototype.autoSize = true;
/**
* Variable: selectText
*
* Specifies if the text should be selected when editing starts. Default is
* true.
*/
mxCellEditor.prototype.selectText = true;
/**
* Variable: emptyLabelText
*
* Text to be displayed for empty labels. Default is '' or '<br>' in Firefox as
* a workaround for the missing cursor bug for empty content editable. This can
* be set to eg. "[Type Here]" to easier visualize editing of empty labels. The
* value is only displayed before the first keystroke and is never used as the
* actual editing value.
*/
mxCellEditor.prototype.emptyLabelText = (mxClient.IS_FF) ? '<br>' : '';
/**
* Variable: escapeCancelsEditing
*
* If true, pressing the escape key will stop editing and not accept the new
* value. Change this to false to accept the new value on escape, and cancel
* editing on Shift+Escape instead. Default is true.
*/
mxCellEditor.prototype.escapeCancelsEditing = true;
/**
* Variable: textNode
*
* Reference to the label DOM node that has been hidden.
*/
mxCellEditor.prototype.textNode = '';
/**
* Variable: zIndex
*
* Specifies the zIndex for the textarea. Default is 5.
*/
mxCellEditor.prototype.zIndex = 5;
/**
* Variable: minResize
*
* Defines the minimum width and height to be used in <resize>. Default is 0x20px.
*/
mxCellEditor.prototype.minResize = new mxRectangle(0, 20);
/**
* Variable: wordWrapPadding
*
* Correction factor for word wrapping width. Default is 2 in quirks, 0 in IE
* 11 and 1 in all other browsers and modes.
*/
mxCellEditor.prototype.wordWrapPadding = (mxClient.IS_QUIRKS) ? 2 : (!mxClient.IS_IE11) ? 1 : 0;
/**
* Variable: blurEnabled
*
* If <focusLost> should be called if <textarea> loses the focus. Default is false.
*/
mxCellEditor.prototype.blurEnabled = false;
/**
* Variable: initialValue
*
* Holds the initial editing value to check if the current value was modified.
*/
mxCellEditor.prototype.initialValue = null;
/**
* Variable: align
*
* Holds the current temporary horizontal alignment for the cell style. If this
* is modified then the current text alignment is changed and the cell style is
* updated when the value is applied.
*/
mxCellEditor.prototype.align = null;
/**
* Function: init
*
* Creates the <textarea> and installs the event listeners. The key handler
* updates the <modified> state.
*/
mxCellEditor.prototype.init = function ()
{
this.textarea = document.createElement('div');
this.textarea.className = 'mxCellEditor mxPlainTextEditor';
this.textarea.contentEditable = true;
// Workaround for selection outside of DIV if height is 0
if (mxClient.IS_GC)
{
this.textarea.style.minHeight = '1em';
}
this.textarea.style.position = ((this.isLegacyEditor())) ? 'absolute' : 'relative';
this.installListeners(this.textarea);
};
/**
* Function: applyValue
*
* Called in <stopEditing> if cancel is false to invoke <mxGraph.labelChanged>.
*/
mxCellEditor.prototype.applyValue = function(state, value)
{
this.graph.labelChanged(state.cell, value, this.trigger);
};
/**
* Function: setAlign
*
* Sets the temporary horizontal alignment for the current editing session.
*/
mxCellEditor.prototype.setAlign = function (align)
{
if (this.textarea != null)
{
this.textarea.style.textAlign = align;
}
this.align = align;
this.resize();
};
/**
* Function: getInitialValue
*
* Gets the initial editing value for the given cell.
*/
mxCellEditor.prototype.getInitialValue = function(state, trigger)
{
var result = mxUtils.htmlEntities(this.graph.getEditingValue(state.cell, trigger), false);
// Workaround for trailing line breaks being ignored in the editor
if (!mxClient.IS_QUIRKS && document.documentMode != 8 && document.documentMode != 9 &&
document.documentMode != 10)
{
result = mxUtils.replaceTrailingNewlines(result, '<div><br></div>');
}
return result.replace(/\n/g, '<br>');
};
/**
* Function: getCurrentValue
*
* Returns the current editing value.
*/
mxCellEditor.prototype.getCurrentValue = function(state)
{
return mxUtils.extractTextWithWhitespace(this.textarea.childNodes);
};
/**
* Function: isCancelEditingKeyEvent
*
* Returns true if <escapeCancelsEditing> is true and shift, control and meta
* are not pressed.
*/
mxCellEditor.prototype.isCancelEditingKeyEvent = function(evt)
{
return this.escapeCancelsEditing || mxEvent.isShiftDown(evt) || mxEvent.isControlDown(evt) || mxEvent.isMetaDown(evt);
};
/**
* Function: installListeners
*
* Installs listeners for focus, change and standard key event handling.
*/
mxCellEditor.prototype.installListeners = function(elt)
{
// Applies value if text is dragged
// LATER: Gesture mouse events ignored for starting move
mxEvent.addListener(elt, 'dragstart', mxUtils.bind(this, function(evt)
{
this.graph.stopEditing(false);
mxEvent.consume(evt);
}));
// Applies value if focus is lost
mxEvent.addListener(elt, 'blur', mxUtils.bind(this, function(evt)
{
if (this.blurEnabled)
{
this.focusLost(evt);
}
}));
// Updates modified state and handles placeholder text
mxEvent.addListener(elt, 'keydown', mxUtils.bind(this, function(evt)
{
if (!mxEvent.isConsumed(evt))
{
if (this.isStopEditingEvent(evt))
{
this.graph.stopEditing(false);
mxEvent.consume(evt);
}
else if (evt.keyCode == 27 /* Escape */)
{
this.graph.stopEditing(this.isCancelEditingKeyEvent(evt));
mxEvent.consume(evt);
}
}
}));
// Keypress only fires if printable key was pressed and handles removing the empty placeholder
var keypressHandler = mxUtils.bind(this, function(evt)
{
if (this.editingCell != null)
{
// Clears the initial empty label on the first keystroke
// and workaround for FF which fires keypress for delete and backspace
if (this.clearOnChange && elt.innerHTML == this.getEmptyLabelText() &&
(!mxClient.IS_FF || (evt.keyCode != 8 /* Backspace */ && evt.keyCode != 46 /* Delete */)))
{
this.clearOnChange = false;
elt.innerHTML = '';
}
}
});
mxEvent.addListener(elt, 'keypress', keypressHandler);
mxEvent.addListener(elt, 'paste', keypressHandler);
// Handler for updating the empty label text value after a change
var keyupHandler = mxUtils.bind(this, function(evt)
{
if (this.editingCell != null)
{
// Uses an optional text value for sempty labels which is cleared
// when the first keystroke appears. This makes it easier to see
// that a label is being edited even if the label is empty.
// In Safari and FF, an empty text is represented by <BR> which isn't enough to force a valid size
if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
{
this.textarea.innerHTML = this.getEmptyLabelText();
this.clearOnChange = this.textarea.innerHTML.length > 0;
}
else
{
this.clearOnChange = false;
}
}
});
mxEvent.addListener(elt, (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keyup', keyupHandler);
mxEvent.addListener(elt, 'cut', keyupHandler);
mxEvent.addListener(elt, 'paste', keyupHandler);
// Adds automatic resizing of the textbox while typing using input, keyup and/or DOM change events
var evtName = (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keydown';
var resizeHandler = mxUtils.bind(this, function(evt)
{
if (this.editingCell != null && this.autoSize && !mxEvent.isConsumed(evt))
{
// Asynchronous is needed for keydown and shows better results for input events overall
// (ie non-blocking and cases where the offsetWidth/-Height was wrong at this time)
if (this.resizeThread != null)
{
window.clearTimeout(this.resizeThread);
}
this.resizeThread = window.setTimeout(mxUtils.bind(this, function()
{
this.resizeThread = null;
this.resize();
}), 0);
}
});
mxEvent.addListener(elt, evtName, resizeHandler);
mxEvent.addListener(window, 'resize', resizeHandler);
if (document.documentMode >= 9)
{
mxEvent.addListener(elt, 'DOMNodeRemoved', resizeHandler);
mxEvent.addListener(elt, 'DOMNodeInserted', resizeHandler);
}
else
{
mxEvent.addListener(elt, 'cut', resizeHandler);
mxEvent.addListener(elt, 'paste', resizeHandler);
}
};
/**
* Function: isStopEditingEvent
*
* Returns true if the given keydown event should stop cell editing. This
* returns true if F2 is pressed of if <mxGraph.enterStopsCellEditing> is true
* and enter is pressed without control or shift.
*/
mxCellEditor.prototype.isStopEditingEvent = function(evt)
{
return evt.keyCode == 113 /* F2 */ || (this.graph.isEnterStopsCellEditing() &&
evt.keyCode == 13 /* Enter */ && !mxEvent.isControlDown(evt) &&
!mxEvent.isShiftDown(evt));
};
/**
* Function: isEventSource
*
* Returns true if this editor is the source for the given native event.
*/
mxCellEditor.prototype.isEventSource = function(evt)
{
return mxEvent.getSource(evt) == this.textarea;
};
/**
* Function: resize
*
* Returns <modified>.
*/
mxCellEditor.prototype.resize = function()
{
var state = this.graph.getView().getState(this.editingCell);
if (state == null)
{
this.stopEditing(true);
}
else if (this.textarea != null)
{
var isEdge = this.graph.getModel().isEdge(state.cell);
var scale = this.graph.getView().scale;
var m = null;
if (!this.autoSize || (state.style[mxConstants.STYLE_OVERFLOW] == 'fill'))
{
// Specifies the bounds of the editor box
this.bounds = this.getEditorBounds(state);
this.textarea.style.width = Math.round(this.bounds.width / scale) + 'px';
this.textarea.style.height = Math.round(this.bounds.height / scale) + 'px';
// FIXME: Offset when scaled
if (document.documentMode == 8 || mxClient.IS_QUIRKS)
{
this.textarea.style.left = Math.round(this.bounds.x) + 'px';
this.textarea.style.top = Math.round(this.bounds.y) + 'px';
}
else
{
this.textarea.style.left = Math.max(0, Math.round(this.bounds.x + 1)) + 'px';
this.textarea.style.top = Math.max(0, Math.round(this.bounds.y + 1)) + 'px';
}
// Installs native word wrapping and avoids word wrap for empty label placeholder
if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
this.textarea.innerHTML != this.getEmptyLabelText())
{
this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
this.textarea.style.whiteSpace = 'normal';
if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
{
this.textarea.style.width = Math.round(this.bounds.width / scale) + this.wordWrapPadding + 'px';
}
}
else
{
this.textarea.style.whiteSpace = 'nowrap';
if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
{
this.textarea.style.width = '';
}
}
}
else
{
var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
m = (state.text != null && this.align == null) ? state.text.margin : null;
if (m == null)
{
m = mxUtils.getAlignmentAsPoint(this.align || mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER),
mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE));
}
if (isEdge)
{
this.bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y, 0, 0);
if (lw != null)
{
var tmp = (parseFloat(lw) + 2) * scale;
this.bounds.width = tmp;
this.bounds.x += m.x * tmp;
}
}
else
{
var bds = mxRectangle.fromRectangle(state);
var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
bds = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(bds) : bds;
if (lw != null)
{
bds.width = parseFloat(lw) * scale;
}
if (!state.view.graph.cellRenderer.legacySpacing || state.style[mxConstants.STYLE_OVERFLOW] != 'width')
{
var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 2) * scale;
var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
bds = new mxRectangle(bds.x + spacingLeft, bds.y + spacingTop,
bds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (spacingLeft + spacingRight) : 0),
bds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (spacingTop + spacingBottom) : 0));
}
this.bounds = new mxRectangle(bds.x + state.absoluteOffset.x, bds.y + state.absoluteOffset.y, bds.width, bds.height);
}
// Needed for word wrap inside text blocks with oversize lines to match the final result where
// the width of the longest line is used as the reference for text alignment in the cell
// TODO: Fix word wrapping preview for edge labels in helloworld.html
if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
this.textarea.innerHTML != this.getEmptyLabelText())
{
this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
this.textarea.style.whiteSpace = 'normal';
// Forces automatic reflow if text is removed from an oversize label and normal word wrap
var tmp = Math.round(this.bounds.width / ((document.documentMode == 8) ? scale : scale)) + this.wordWrapPadding;
if (this.textarea.style.position != 'relative')
{
this.textarea.style.width = tmp + 'px';
if (this.textarea.scrollWidth > tmp)
{
this.textarea.style.width = this.textarea.scrollWidth + 'px';
}
}
else
{
this.textarea.style.maxWidth = tmp + 'px';
}
}
else
{
// KNOWN: Trailing cursor in IE9 quirks mode is not visible
this.textarea.style.whiteSpace = 'nowrap';
this.textarea.style.width = '';
}
// LATER: Keep in visible area, add fine tuning for pixel precision
// Workaround for wrong measuring in IE8 standards
if (document.documentMode == 8)
{
this.textarea.style.zoom = '1';
this.textarea.style.height = 'auto';
}
var ow = this.textarea.scrollWidth;
var oh = this.textarea.scrollHeight;
// TODO: Update CSS width and height if smaller than minResize or remove minResize
//if (this.minResize != null)
//{
// ow = Math.max(ow, this.minResize.width);
// oh = Math.max(oh, this.minResize.height);
//}
// LATER: Keep in visible area, add fine tuning for pixel precision
if (document.documentMode == 8)
{
// LATER: Scaled wrapping and position is wrong in IE8
this.textarea.style.left = Math.max(0, Math.ceil((this.bounds.x - m.x * (this.bounds.width - (ow + 1) * scale) + ow * (scale - 1) * 0 + (m.x + 0.5) * 2) / scale)) + 'px';
this.textarea.style.top = Math.max(0, Math.ceil((this.bounds.y - m.y * (this.bounds.height - (oh + 0.5) * scale) + oh * (scale - 1) * 0 + Math.abs(m.y + 0.5) * 1) / scale)) + 'px';
// Workaround for wrong event handling width and height
this.textarea.style.width = Math.round(ow * scale) + 'px';
this.textarea.style.height = Math.round(oh * scale) + 'px';
}
else if (mxClient.IS_QUIRKS)
{
this.textarea.style.left = Math.max(0, Math.ceil(this.bounds.x - m.x * (this.bounds.width - (ow + 1) * scale) + ow * (scale - 1) * 0 + (m.x + 0.5) * 2)) + 'px';
this.textarea.style.top = Math.max(0, Math.ceil(this.bounds.y - m.y * (this.bounds.height - (oh + 0.5) * scale) + oh * (scale - 1) * 0 + Math.abs(m.y + 0.5) * 1)) + 'px';
}
else
{
this.textarea.style.left = Math.max(0, Math.round(this.bounds.x - m.x * (this.bounds.width - 2)) + 1) + 'px';
this.textarea.style.top = Math.max(0, Math.round(this.bounds.y - m.y * (this.bounds.height - 4) + ((m.y == -1) ? 3 : 0)) + 1) + 'px';
}
}
if (mxClient.IS_VML)
{
this.textarea.style.zoom = scale;
}
else
{
mxUtils.setPrefixedStyle(this.textarea.style, 'transformOrigin', '0px 0px');
mxUtils.setPrefixedStyle(this.textarea.style, 'transform',
'scale(' + scale + ',' + scale + ')' + ((m == null) ? '' :
' translate(' + (m.x * 100) + '%,' + (m.y * 100) + '%)'));
}
}
};
/**
* Function: focusLost
*
* Called if the textarea has lost focus.
*/
mxCellEditor.prototype.focusLost = function()
{
this.stopEditing(!this.graph.isInvokesStopCellEditing());
};
/**
* Function: getBackgroundColor
*
* Returns the background color for the in-place editor. This implementation
* always returns null.
*/
mxCellEditor.prototype.getBackgroundColor = function(state)
{
return null;
};
/**
* Function: isLegacyEditor
*
* Returns true if max-width is not supported or if the SVG root element in
* in the graph does not have CSS position absolute. In these cases the text
* editor must use CSS position absolute to avoid an offset but it will have
* a less accurate line wrapping width during the text editing preview. This
* implementation returns true for IE8- and quirks mode or if the CSS position
* of the SVG element is not absolute.
*/
mxCellEditor.prototype.isLegacyEditor = function()
{
if (mxClient.IS_VML)
{
return true;
}
else
{
var absoluteRoot = false;
if (mxClient.IS_SVG)
{
var root = this.graph.view.getDrawPane().ownerSVGElement;
if (root != null)
{
absoluteRoot = mxUtils.getCurrentStyle(root).position == 'absolute';
}
}
return !absoluteRoot;
}
};
/**
* Function: startEditing
*
* Starts the editor for the given cell.
*
* Parameters:
*
* cell - <mxCell> to start editing.
* trigger - Optional mouse event that triggered the editor.
*/
mxCellEditor.prototype.startEditing = function(cell, trigger)
{
this.stopEditing(true);
this.align = null;
// Creates new textarea instance
if (this.textarea == null)
{
this.init();
}
if (this.graph.tooltipHandler != null)
{
this.graph.tooltipHandler.hideTooltip();
}
var state = this.graph.getView().getState(cell);
if (state != null)
{
// Configures the style of the in-place editor
var scale = this.graph.getView().scale;
var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE);
var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY);
var color = mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, 'black');
var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT);
var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD;
var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC;
var uline = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE;
this.textarea.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT;
this.textarea.style.backgroundColor = this.getBackgroundColor(state);
this.textarea.style.textDecoration = (uline) ? 'underline' : '';
this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal';
this.textarea.style.fontStyle = (italic) ? 'italic' : '';
this.textarea.style.fontSize = Math.round(size) + 'px';
this.textarea.style.zIndex = this.zIndex;
this.textarea.style.fontFamily = family;
this.textarea.style.textAlign = align;
this.textarea.style.outline = 'none';
this.textarea.style.color = color;
var dir = this.textDirection = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
if (dir == mxConstants.TEXT_DIRECTION_AUTO)
{
if (state != null && state.text != null && state.text.dialect != mxConstants.DIALECT_STRICTHTML &&
!mxUtils.isNode(state.text.value))
{
dir = state.text.getAutoDirection();
}
}
if (dir == mxConstants.TEXT_DIRECTION_LTR || dir == mxConstants.TEXT_DIRECTION_RTL)
{
this.textarea.setAttribute('dir', dir);
}
else
{
this.textarea.removeAttribute('dir');
}
// Sets the initial editing value
this.textarea.innerHTML = this.getInitialValue(state, trigger) || '';
this.initialValue = this.textarea.innerHTML;
// Uses an optional text value for empty labels which is cleared
// when the first keystroke appears. This makes it easier to see
// that a label is being edited even if the label is empty.
if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
{
this.textarea.innerHTML = this.getEmptyLabelText();
this.clearOnChange = true;
}
else
{
this.clearOnChange = this.textarea.innerHTML == this.getEmptyLabelText();
}
this.graph.container.appendChild(this.textarea);
// Update this after firing all potential events that could update the cleanOnChange flag
this.editingCell = cell;
this.trigger = trigger;
this.textNode = null;
if (state.text != null && this.isHideLabel(state))
{
this.textNode = state.text.node;
this.textNode.style.visibility = 'hidden';
}
// Workaround for initial offsetHeight not ready for heading in markup
if (this.autoSize && (this.graph.model.isEdge(state.cell) || state.style[mxConstants.STYLE_OVERFLOW] != 'fill'))
{
window.setTimeout(mxUtils.bind(this, function()
{
this.resize();
}), 0);
}
this.resize();
// Workaround for NS_ERROR_FAILURE in FF
try
{
// Prefers blinking cursor over no selected text if empty
this.textarea.focus();
if (this.isSelectText() && this.textarea.innerHTML.length > 0 &&
(this.textarea.innerHTML != this.getEmptyLabelText() || !this.clearOnChange))
{
document.execCommand('selectAll', false, null);
}
}
catch (e)
{
// ignore
}
}
};
/**
* Function: isSelectText
*
* Returns <selectText>.
*/
mxCellEditor.prototype.isSelectText = function()
{
return this.selectText;
};
/**
* Function: isSelectText
*
* Returns <selectText>.
*/
mxCellEditor.prototype.clearSelection = function()
{
var selection = null;
if (window.getSelection)
{
selection = window.getSelection();
}
else if (document.selection)
{
selection = document.selection;
}
if (selection != null)
{
if (selection.empty)
{
selection.empty();
}
else if (selection.removeAllRanges)
{
selection.removeAllRanges();
}
}
};
/**
* Function: stopEditing
*
* Stops the editor and applies the value if cancel is false.
*/
mxCellEditor.prototype.stopEditing = function(cancel)
{
cancel = cancel || false;
if (this.editingCell != null)
{
if (this.textNode != null)
{
this.textNode.style.visibility = 'visible';
this.textNode = null;
}
var state = (!cancel) ? this.graph.view.getState(this.editingCell) : null;
var initial = this.initialValue;
this.initialValue = null;
this.editingCell = null;
this.trigger = null;
this.bounds = null;
this.textarea.blur();
this.clearSelection();
if (this.textarea.parentNode != null)
{
this.textarea.parentNode.removeChild(this.textarea);
}
if (this.clearOnChange && this.textarea.innerHTML == this.getEmptyLabelText())
{
this.textarea.innerHTML = '';
this.clearOnChange = false;
}
if (state != null && (this.textarea.innerHTML != initial || this.align != null))
{
this.prepareTextarea();
var value = this.getCurrentValue(state);
this.graph.getModel().beginUpdate();
try
{
if (value != null)
{
this.applyValue(state, value);
}
if (this.align != null)
{
this.graph.setCellStyles(mxConstants.STYLE_ALIGN, this.align, [state.cell]);
}
}
finally
{
this.graph.getModel().endUpdate();
}
}
// Forces new instance on next edit for undo history reset
mxEvent.release(this.textarea);
this.textarea = null;
this.align = null;
}
};
/**
* Function: prepareTextarea
*
* Prepares the textarea for getting its value in <stopEditing>.
* This implementation removes the extra trailing linefeed in Firefox.
*/
mxCellEditor.prototype.prepareTextarea = function()
{
if (this.textarea.lastChild != null &&
this.textarea.lastChild.nodeName == 'BR')
{
this.textarea.removeChild(this.textarea.lastChild);
}
};
/**
* Function: isHideLabel
*
* Returns true if the label should be hidden while the cell is being
* edited.
*/
mxCellEditor.prototype.isHideLabel = function(state)
{
return true;
};
/**
* Function: getMinimumSize
*
* Returns the minimum width and height for editing the given state.
*/
mxCellEditor.prototype.getMinimumSize = function(state)
{
var scale = this.graph.getView().scale;
return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20,
(this.textarea.style.textAlign == 'left') ? 120 : 40);
};
/**
* Function: getEditorBounds
*
* Returns the <mxRectangle> that defines the bounds of the editor.
*/
mxCellEditor.prototype.getEditorBounds = function(state)
{
var isEdge = this.graph.getModel().isEdge(state.cell);
var scale = this.graph.getView().scale;
var minSize = this.getMinimumSize(state);
var minWidth = minSize.width;
var minHeight = minSize.height;
var result = null;
if (!isEdge && state.view.graph.cellRenderer.legacySpacing && state.style[mxConstants.STYLE_OVERFLOW] == 'fill')
{
result = state.shape.getLabelBounds(mxRectangle.fromRectangle(state));
}
else
{
var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 0) * scale;
var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
result = new mxRectangle(state.x, state.y,
Math.max(minWidth, state.width - spacingLeft - spacingRight),
Math.max(minHeight, state.height - spacingTop - spacingBottom));
var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
result = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(result) : result;
if (isEdge)
{
result.x = state.absoluteOffset.x;
result.y = state.absoluteOffset.y;
if (state.text != null && state.text.boundingBox != null)
{
// Workaround for label containing just spaces in which case
// the bounding box location contains negative numbers
if (state.text.boundingBox.x > 0)
{
result.x = state.text.boundingBox.x;
}
if (state.text.boundingBox.y > 0)
{
result.y = state.text.boundingBox.y;
}
}
}
else if (state.text != null && state.text.boundingBox != null)
{
result.x = Math.min(result.x, state.text.boundingBox.x);
result.y = Math.min(result.y, state.text.boundingBox.y);
}
result.x += spacingLeft;
result.y += spacingTop;
if (state.text != null && state.text.boundingBox != null)
{
if (!isEdge)
{
result.width = Math.max(result.width, state.text.boundingBox.width);
result.height = Math.max(result.height, state.text.boundingBox.height);
}
else
{
result.width = Math.max(minWidth, state.text.boundingBox.width);
result.height = Math.max(minHeight, state.text.boundingBox.height);
}
}
// Applies the horizontal and vertical label positions
if (this.graph.getModel().isVertex(state.cell))
{
var horizontal = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
if (horizontal == mxConstants.ALIGN_LEFT)
{
result.x -= state.width;
}
else if (horizontal == mxConstants.ALIGN_RIGHT)
{
result.x += state.width;
}
var vertical = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
if (vertical == mxConstants.ALIGN_TOP)
{
result.y -= state.height;
}
else if (vertical == mxConstants.ALIGN_BOTTOM)
{
result.y += state.height;
}
}
}
return new mxRectangle(Math.round(result.x), Math.round(result.y), Math.round(result.width), Math.round(result.height));
};
/**
* Function: getEmptyLabelText
*
* Returns the initial label value to be used of the label of the given
* cell is empty. This label is displayed and cleared on the first keystroke.
* This implementation returns <emptyLabelText>.
*
* Parameters:
*
* cell - <mxCell> for which a text for an empty editing box should be
* returned.
*/
mxCellEditor.prototype.getEmptyLabelText = function (cell)
{
return this.emptyLabelText;
};
/**
* Function: getEditingCell
*
* Returns the cell that is currently being edited or null if no cell is
* being edited.
*/
mxCellEditor.prototype.getEditingCell = function ()
{
return this.editingCell;
};
/**
* Function: destroy
*
* Destroys the editor and removes all associated resources.
*/
mxCellEditor.prototype.destroy = function ()
{
if (this.textarea != null)
{
mxEvent.release(this.textarea);
if (this.textarea.parentNode != null)
{
this.textarea.parentNode.removeChild(this.textarea);
}
this.textarea = null;
}
if (this.changeHandler != null)
{
this.graph.getModel().removeListener(this.changeHandler);
this.changeHandler = null;
}
if (this.zoomHandler)
{
this.graph.view.removeListener(this.zoomHandler);
this.zoomHandler = null;
}
};
/**
* Copyright (c) 2006-2017, JGraph Ltd
* Copyright (c) 2006-2017, Gaudenz Alder
*/
/**
* Class: mxCellRenderer
*
* Renders cells into a document object model. The <defaultShapes> is a global
* map of shapename, constructor pairs that is used in all instances. You can
* get a list of all available shape names using the following code.
*
* In general the cell renderer is in charge of creating, redrawing and
* destroying the shape and label associated with a cell state, as well as
* some other graphical objects, namely controls and overlays. The shape
* hieararchy in the display (ie. the hierarchy in which the DOM nodes
* appear in the document) does not reflect the cell hierarchy. The shapes
* are a (flat) sequence of shapes and labels inside the draw pane of the
* graph view, with some exceptions, namely the HTML labels being placed
* directly inside the graph container for certain browsers.
*
* (code)
* mxLog.show();
* for (var i in mxCellRenderer.defaultShapes)
* {
* mxLog.debug(i);
* }
* (end)
*
* Constructor: mxCellRenderer
*
* Constructs a new cell renderer with the following built-in shapes:
* arrow, rectangle, ellipse, rhombus, image, line, label, cylinder,
* swimlane, connector, actor and cloud.
*/
function mxCellRenderer() { };
/**
* Variable: defaultShapes
*
* Static array that contains the globally registered shapes which are
* known to all instances of this class. For adding new shapes you should
* use the static <mxCellRenderer.registerShape> function.
*/
mxCellRenderer.defaultShapes = new Object();
/**
* Variable: defaultEdgeShape
*
* Defines the default shape for edges. Default is <mxConnector>.
*/
mxCellRenderer.prototype.defaultEdgeShape = mxConnector;
/**
* Variable: defaultVertexShape
*
* Defines the default shape for vertices. Default is <mxRectangleShape>.
*/
mxCellRenderer.prototype.defaultVertexShape = mxRectangleShape;
/**
* Variable: defaultTextShape
*
* Defines the default shape for labels. Default is <mxText>.
*/
mxCellRenderer.prototype.defaultTextShape = mxText;
/**
* Variable: legacyControlPosition
*
* Specifies if the folding icon should ignore the horizontal
* orientation of a swimlane. Default is true.
*/
mxCellRenderer.prototype.legacyControlPosition = true;
/**
* Variable: legacySpacing
*
* Specifies if spacing and label position should be ignored if overflow is
* fill or width. Default is true for backwards compatiblity.
*/
mxCellRenderer.prototype.legacySpacing = true;
/**
* Variable: antiAlias
*
* Anti-aliasing option for new shapes. Default is true.
*/
mxCellRenderer.prototype.antiAlias = true;
/**
* Variable: minSvgStrokeWidth
*
* Minimum stroke width for SVG output.
*/
mxCellRenderer.prototype.minSvgStrokeWidth = 1;
/**
* Variable: forceControlClickHandler
*
* Specifies if the enabled state of the graph should be ignored in the control
* click handler (to allow folding in disabled graphs). Default is false.
*/
mxCellRenderer.prototype.forceControlClickHandler = false;
/**
* Function: registerShape
*
* Registers the given constructor under the specified key in this instance
* of the renderer.
*
* Example:
*
* (code)
* mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape);
* (end)
*
* Parameters:
*
* key - String representing the shape name.
* shape - Constructor of the <mxShape> subclass.
*/
mxCellRenderer.registerShape = function(key, shape)
{
mxCellRenderer.defaultShapes[key] = shape;
};
// Adds default shapes into the default shapes array
mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape);
mxCellRenderer.registerShape(mxConstants.SHAPE_ELLIPSE, mxEllipse);
mxCellRenderer.registerShape(mxConstants.SHAPE_RHOMBUS, mxRhombus);
mxCellRenderer.registerShape(mxConstants.SHAPE_CYLINDER, mxCylinder);
mxCellRenderer.registerShape(mxConstants.SHAPE_CONNECTOR, mxConnector);
mxCellRenderer.registerShape(mxConstants.SHAPE_ACTOR, mxActor);
mxCellRenderer.registerShape(mxConstants.SHAPE_TRIANGLE, mxTriangle);
mxCellRenderer.registerShape(mxConstants.SHAPE_HEXAGON, mxHexagon);
mxCellRenderer.registerShape(mxConstants.SHAPE_CLOUD, mxCloud);
mxCellRenderer.registerShape(mxConstants.SHAPE_LINE, mxLine);
mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW, mxArrow);
mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW_CONNECTOR, mxArrowConnector);
mxCellRenderer.registerShape(mxConstants.SHAPE_DOUBLE_ELLIPSE, mxDoubleEllipse);
mxCellRenderer.registerShape(mxConstants.SHAPE_SWIMLANE, mxSwimlane);
mxCellRenderer.registerShape(mxConstants.SHAPE_IMAGE, mxImageShape);
mxCellRenderer.registerShape(mxConstants.SHAPE_LABEL, mxLabel);
/**
* Function: initializeShape
*
* Initializes the shape in the given state by calling its init method with
* the correct container after configuring it using <configureShape>.
*
* Parameters:
*
* state - <mxCellState> for which the shape should be initialized.
*/
mxCellRenderer.prototype.initializeShape = function(state)
{
state.shape.dialect = state.view.graph.dialect;
this.configureShape(state);
state.shape.init(state.view.getDrawPane());
};
/**
* Function: createShape
*
* Creates and returns the shape for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the shape should be created.
*/
mxCellRenderer.prototype.createShape = function(state)
{
var shape = null;
if (state.style != null)
{
// Checks if there is a stencil for the name and creates
// a shape instance for the stencil if one exists
var stencil = mxStencilRegistry.getStencil(state.style[mxConstants.STYLE_SHAPE]);
if (stencil != null)
{
shape = new mxShape(stencil);
}
else
{
var ctor = this.getShapeConstructor(state);
shape = new ctor();
}
}
return shape;
};
/**
* Function: createIndicatorShape
*
* Creates the indicator shape for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the indicator shape should be created.
*/
mxCellRenderer.prototype.createIndicatorShape = function(state)
{
state.shape.indicatorShape = this.getShape(state.view.graph.getIndicatorShape(state));
};
/**
* Function: getShape
*
* Returns the shape for the given name from <defaultShapes>.
*/
mxCellRenderer.prototype.getShape = function(name)
{
return (name != null) ? mxCellRenderer.defaultShapes[name] : null;
};
/**
* Function: getShapeConstructor
*
* Returns the constructor to be used for creating the shape.
*/
mxCellRenderer.prototype.getShapeConstructor = function(state)
{
var ctor = this.getShape(state.style[mxConstants.STYLE_SHAPE]);
if (ctor == null)
{
ctor = (state.view.graph.getModel().isEdge(state.cell)) ?
this.defaultEdgeShape : this.defaultVertexShape;
}
return ctor;
};
/**
* Function: configureShape
*
* Configures the shape for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the shape should be configured.
*/
mxCellRenderer.prototype.configureShape = function(state)
{
state.shape.apply(state);
state.shape.image = state.view.graph.getImage(state);
state.shape.indicatorColor = state.view.graph.getIndicatorColor(state);
state.shape.indicatorStrokeColor = state.style[mxConstants.STYLE_INDICATOR_STROKECOLOR];
state.shape.indicatorGradientColor = state.view.graph.getIndicatorGradientColor(state);
state.shape.indicatorDirection = state.style[mxConstants.STYLE_INDICATOR_DIRECTION];
state.shape.indicatorImage = state.view.graph.getIndicatorImage(state);
this.postConfigureShape(state);
};
/**
* Function: postConfigureShape
*
* Replaces any reserved words used for attributes, eg. inherit,
* indicated or swimlane for colors in the shape for the given state.
* This implementation resolves these keywords on the fill, stroke
* and gradient color keys.
*/
mxCellRenderer.prototype.postConfigureShape = function(state)
{
if (state.shape != null)
{
this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR);
this.resolveColor(state, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR);
this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR);
this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR);
this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR);
}
};
/**
* Function: checkPlaceholderStyles
*
* Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets
* the respective color on the shape.
*/
mxCellRenderer.prototype.checkPlaceholderStyles = function(state)
{
// LATER: Check if the color has actually changed
if (state.style != null)
{
var values = ['inherit', 'swimlane', 'indicated'];
var styles = [mxConstants.STYLE_FILLCOLOR, mxConstants.STYLE_STROKECOLOR, mxConstants.STYLE_GRADIENTCOLOR];
for (var i = 0; i < styles.length; i++)
{
if (mxUtils.indexOf(values, state.style[styles[i]]) >= 0)
{
return true;
}
}
}
return false;
};
/**
* Function: resolveColor
*
* Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets
* the respective color on the shape.
*/
mxCellRenderer.prototype.resolveColor = function(state, field, key)
{
var value = state.shape[field];
var graph = state.view.graph;
var referenced = null;
if (value == 'inherit')
{
referenced = graph.model.getParent(state.cell);
}
else if (value == 'swimlane')
{
state.shape[field] = (key == mxConstants.STYLE_STROKECOLOR) ? '#000000' : '#ffffff';
if (graph.model.getTerminal(state.cell, false) != null)
{
referenced = graph.model.getTerminal(state.cell, false);
}
else
{
referenced = state.cell;
}
referenced = graph.getSwimlane(referenced);
key = graph.swimlaneIndicatorColorAttribute;
}
else if (value == 'indicated')
{
state.shape[field] = state.shape.indicatorColor;
}
if (referenced != null)
{
var rstate = graph.getView().getState(referenced);
state.shape[field] = null;
if (rstate != null)
{
if (rstate.shape != null && field != 'indicatorColor')
{
state.shape[field] = rstate.shape[field];
}
else
{
state.shape[field] = rstate.style[key];
}
}
}
};
/**
* Function: getLabelValue
*
* Returns the value to be used for the label.
*
* Parameters:
*
* state - <mxCellState> for which the label should be created.
*/
mxCellRenderer.prototype.getLabelValue = function(state)
{
return state.view.graph.getLabel(state.cell);
};
/**
* Function: createLabel
*
* Creates the label for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the label should be created.
*/
mxCellRenderer.prototype.createLabel = function(state, value)
{
var graph = state.view.graph;
var isEdge = graph.getModel().isEdge(state.cell);
if (state.style[mxConstants.STYLE_FONTSIZE] > 0 || state.style[mxConstants.STYLE_FONTSIZE] == null)
{
// Avoids using DOM node for empty labels
var isForceHtml = (graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value)));
state.text = new this.defaultTextShape(value, new mxRectangle(),
(state.style[mxConstants.STYLE_ALIGN] || mxConstants.ALIGN_CENTER),
graph.getVerticalAlign(state),
state.style[mxConstants.STYLE_FONTCOLOR],
state.style[mxConstants.STYLE_FONTFAMILY],
state.style[mxConstants.STYLE_FONTSIZE],
state.style[mxConstants.STYLE_FONTSTYLE],
state.style[mxConstants.STYLE_SPACING],
state.style[mxConstants.STYLE_SPACING_TOP],
state.style[mxConstants.STYLE_SPACING_RIGHT],
state.style[mxConstants.STYLE_SPACING_BOTTOM],
state.style[mxConstants.STYLE_SPACING_LEFT],
state.style[mxConstants.STYLE_HORIZONTAL],
state.style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR],
state.style[mxConstants.STYLE_LABEL_BORDERCOLOR],
graph.isWrapping(state.cell) && graph.isHtmlLabel(state.cell),
graph.isLabelClipped(state.cell),
state.style[mxConstants.STYLE_OVERFLOW],
state.style[mxConstants.STYLE_LABEL_PADDING],
mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION));
state.text.opacity = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100);
state.text.dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect;
state.text.style = state.style;
state.text.state = state;
this.initializeLabel(state, state.text);
// Workaround for touch devices routing all events for a mouse gesture
// (down, move, up) via the initial DOM node. IE additionally redirects
// the event via the initial DOM node but the event source is the node
// under the mouse, so we need to check if this is the case and force
// getCellAt for the subsequent mouseMoves and the final mouseUp.
var forceGetCell = false;
var getState = function(evt)
{
var result = state;
if (mxClient.IS_TOUCH || forceGetCell)
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
// Dispatches the drop event to the graph which
// consumes and executes the source function
var pt = mxUtils.convertPoint(graph.container, x, y);
result = graph.view.getState(graph.getCellAt(pt.x, pt.y));
}
return result;
};
// TODO: Add handling for special touch device gestures
mxEvent.addGestureListeners(state.text.node,
mxUtils.bind(this, function(evt)
{
if (this.isLabelEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
forceGetCell = graph.dialect != mxConstants.DIALECT_SVG &&
mxEvent.getSource(evt).nodeName == 'IMG';
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isLabelEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isLabelEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
forceGetCell = false;
}
})
);
// Uses double click timeout in mxGraph for quirks mode
if (graph.nativeDblClickEnabled)
{
mxEvent.addListener(state.text.node, 'dblclick',
mxUtils.bind(this, function(evt)
{
if (this.isLabelEvent(state, evt))
{
graph.dblClick(evt, state.cell);
mxEvent.consume(evt);
}
})
);
}
}
};
/**
* Function: initializeLabel
*
* Initiailzes the label with a suitable container.
*
* Parameters:
*
* state - <mxCellState> whose label should be initialized.
*/
mxCellRenderer.prototype.initializeLabel = function(state, shape)
{
if (mxClient.IS_SVG && mxClient.NO_FO && shape.dialect != mxConstants.DIALECT_SVG)
{
shape.init(state.view.graph.container);
}
else
{
shape.init(state.view.getDrawPane());
}
};
/**
* Function: createCellOverlays
*
* Creates the actual shape for showing the overlay for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the overlay should be created.
*/
mxCellRenderer.prototype.createCellOverlays = function(state)
{
var graph = state.view.graph;
var overlays = graph.getCellOverlays(state.cell);
var dict = null;
if (overlays != null)
{
dict = new mxDictionary();
for (var i = 0; i < overlays.length; i++)
{
var shape = (state.overlays != null) ? state.overlays.remove(overlays[i]) : null;
if (shape == null)
{
var tmp = new mxImageShape(new mxRectangle(), overlays[i].image.src);
tmp.dialect = state.view.graph.dialect;
tmp.preserveImageAspect = false;
tmp.overlay = overlays[i];
this.initializeOverlay(state, tmp);
this.installCellOverlayListeners(state, overlays[i], tmp);
if (overlays[i].cursor != null)
{
tmp.node.style.cursor = overlays[i].cursor;
}
dict.put(overlays[i], tmp);
}
else
{
dict.put(overlays[i], shape);
}
}
}
// Removes unused
if (state.overlays != null)
{
state.overlays.visit(function(id, shape)
{
shape.destroy();
});
}
state.overlays = dict;
};
/**
* Function: initializeOverlay
*
* Initializes the given overlay.
*
* Parameters:
*
* state - <mxCellState> for which the overlay should be created.
* overlay - <mxImageShape> that represents the overlay.
*/
mxCellRenderer.prototype.initializeOverlay = function(state, overlay)
{
overlay.init(state.view.getOverlayPane());
};
/**
* Function: installOverlayListeners
*
* Installs the listeners for the given <mxCellState>, <mxCellOverlay> and
* <mxShape> that represents the overlay.
*/
mxCellRenderer.prototype.installCellOverlayListeners = function(state, overlay, shape)
{
var graph = state.view.graph;
mxEvent.addListener(shape.node, 'click', function (evt)
{
if (graph.isEditing())
{
graph.stopEditing(!graph.isInvokesStopCellEditing());
}
overlay.fireEvent(new mxEventObject(mxEvent.CLICK,
'event', evt, 'cell', state.cell));
});
mxEvent.addGestureListeners(shape.node,
function (evt)
{
mxEvent.consume(evt);
},
function (evt)
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE,
new mxMouseEvent(evt, state));
});
if (mxClient.IS_TOUCH)
{
mxEvent.addListener(shape.node, 'touchend', function (evt)
{
overlay.fireEvent(new mxEventObject(mxEvent.CLICK,
'event', evt, 'cell', state.cell));
});
}
};
/**
* Function: createControl
*
* Creates the control for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the control should be created.
*/
mxCellRenderer.prototype.createControl = function(state)
{
var graph = state.view.graph;
var image = graph.getFoldingImage(state);
if (graph.foldingEnabled && image != null)
{
if (state.control == null)
{
var b = new mxRectangle(0, 0, image.width, image.height);
state.control = new mxImageShape(b, image.src);
state.control.preserveImageAspect = false;
state.control.dialect = graph.dialect;
this.initControl(state, state.control, true, this.createControlClickHandler(state));
}
}
else if (state.control != null)
{
state.control.destroy();
state.control = null;
}
};
/**
* Function: createControlClickHandler
*
* Hook for creating the click handler for the folding icon.
*
* Parameters:
*
* state - <mxCellState> whose control click handler should be returned.
*/
mxCellRenderer.prototype.createControlClickHandler = function(state)
{
var graph = state.view.graph;
return mxUtils.bind(this, function (evt)
{
if (this.forceControlClickHandler || graph.isEnabled())
{
var collapse = !graph.isCellCollapsed(state.cell);
graph.foldCells(collapse, false, [state.cell], null, evt);
mxEvent.consume(evt);
}
});
};
/**
* Function: initControl
*
* Initializes the given control and returns the corresponding DOM node.
*
* Parameters:
*
* state - <mxCellState> for which the control should be initialized.
* control - <mxShape> to be initialized.
* handleEvents - Boolean indicating if mousedown and mousemove should fire events via the graph.
* clickHandler - Optional function to implement clicks on the control.
*/
mxCellRenderer.prototype.initControl = function(state, control, handleEvents, clickHandler)
{
var graph = state.view.graph;
// In the special case where the label is in HTML and the display is SVG the image
// should go into the graph container directly in order to be clickable. Otherwise
// it is obscured by the HTML label that overlaps the cell.
var isForceHtml = graph.isHtmlLabel(state.cell) && mxClient.NO_FO &&
graph.dialect == mxConstants.DIALECT_SVG;
if (isForceHtml)
{
control.dialect = mxConstants.DIALECT_PREFERHTML;
control.init(graph.container);
control.node.style.zIndex = 1;
}
else
{
control.init(state.view.getOverlayPane());
}
var node = control.innerNode || control.node;
// Workaround for missing click event on iOS is to check tolerance below
if (clickHandler != null && !mxClient.IS_IOS)
{
if (graph.isEnabled())
{
node.style.cursor = 'pointer';
}
mxEvent.addListener(node, 'click', clickHandler);
}
if (handleEvents)
{
var first = null;
mxEvent.addGestureListeners(node,
function (evt)
{
first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
mxEvent.consume(evt);
},
function (evt)
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state));
},
function (evt)
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, state));
mxEvent.consume(evt);
});
// Uses capture phase for event interception to stop bubble phase
if (clickHandler != null && mxClient.IS_IOS)
{
node.addEventListener('touchend', function(evt)
{
if (first != null)
{
var tol = graph.tolerance;
if (Math.abs(first.x - mxEvent.getClientX(evt)) < tol &&
Math.abs(first.y - mxEvent.getClientY(evt)) < tol)
{
clickHandler.call(clickHandler, evt);
mxEvent.consume(evt);
}
}
}, true);
}
}
return node;
};
/**
* Function: isShapeEvent
*
* Returns true if the event is for the shape of the given state. This
* implementation always returns true.
*
* Parameters:
*
* state - <mxCellState> whose shape fired the event.
* evt - Mouse event which was fired.
*/
mxCellRenderer.prototype.isShapeEvent = function(state, evt)
{
return true;
};
/**
* Function: isLabelEvent
*
* Returns true if the event is for the label of the given state. This
* implementation always returns true.
*
* Parameters:
*
* state - <mxCellState> whose label fired the event.
* evt - Mouse event which was fired.
*/
mxCellRenderer.prototype.isLabelEvent = function(state, evt)
{
return true;
};
/**
* Function: installListeners
*
* Installs the event listeners for the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the event listeners should be isntalled.
*/
mxCellRenderer.prototype.installListeners = function(state)
{
var graph = state.view.graph;
// Workaround for touch devices routing all events for a mouse
// gesture (down, move, up) via the initial DOM node. Same for
// HTML images in all IE versions (VML images are working).
var getState = function(evt)
{
var result = state;
if ((graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG') || mxClient.IS_TOUCH)
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
// Dispatches the drop event to the graph which
// consumes and executes the source function
var pt = mxUtils.convertPoint(graph.container, x, y);
result = graph.view.getState(graph.getCellAt(pt.x, pt.y));
}
return result;
};
mxEvent.addGestureListeners(state.shape.node,
mxUtils.bind(this, function(evt)
{
if (this.isShapeEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isShapeEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isShapeEvent(state, evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
}
})
);
// Uses double click timeout in mxGraph for quirks mode
if (graph.nativeDblClickEnabled)
{
mxEvent.addListener(state.shape.node, 'dblclick',
mxUtils.bind(this, function(evt)
{
if (this.isShapeEvent(state, evt))
{
graph.dblClick(evt, state.cell);
mxEvent.consume(evt);
}
})
);
}
};
/**
* Function: redrawLabel
*
* Redraws the label for the given cell state.
*
* Parameters:
*
* state - <mxCellState> whose label should be redrawn.
*/
mxCellRenderer.prototype.redrawLabel = function(state, forced)
{
var graph = state.view.graph;
var value = this.getLabelValue(state);
var wrapping = graph.isWrapping(state.cell);
var clipping = graph.isLabelClipped(state.cell);
var isForceHtml = (state.view.graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value)));
var dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect;
var overflow = state.style[mxConstants.STYLE_OVERFLOW] || 'visible';
if (state.text != null && (state.text.wrap != wrapping || state.text.clipped != clipping ||
state.text.overflow != overflow || state.text.dialect != dialect))
{
state.text.destroy();
state.text = null;
}
if (state.text == null && value != null && (mxUtils.isNode(value) || value.length > 0))
{
this.createLabel(state, value);
}
else if (state.text != null && (value == null || value.length == 0))
{
state.text.destroy();
state.text = null;
}
if (state.text != null)
{
// Forced is true if the style has changed, so to get the updated
// result in getLabelBounds we apply the new style to the shape
if (forced)
{
// Checks if a full repaint is needed
if (state.text.lastValue != null && this.isTextShapeInvalid(state, state.text))
{
// Forces a full repaint
state.text.lastValue = null;
}
state.text.resetStyles();
state.text.apply(state);
// Special case where value is obtained via hook in graph
state.text.valign = graph.getVerticalAlign(state);
}
var bounds = this.getLabelBounds(state);
var nextScale = this.getTextScale(state);
if (forced || state.text.value != value || state.text.isWrapping != wrapping ||
state.text.overflow != overflow || state.text.isClipping != clipping ||
state.text.scale != nextScale || state.text.dialect != dialect ||
!state.text.bounds.equals(bounds))
{
// Forces an update of the text bounding box
if (state.text.bounds.width != 0 && state.unscaledWidth != null &&
Math.round((state.text.bounds.width /
state.text.scale * nextScale) - bounds.width) != 0)
{
state.unscaledWidth = null;
}
state.text.dialect = dialect;
state.text.value = value;
state.text.bounds = bounds;
state.text.scale = nextScale;
state.text.wrap = wrapping;
state.text.clipped = clipping;
state.text.overflow = overflow;
// Preserves visible state
var vis = state.text.node.style.visibility;
this.redrawLabelShape(state.text);
state.text.node.style.visibility = vis;
}
}
};
/**
* Function: isTextShapeInvalid
*
* Returns true if the style for the text shape has changed.
*
* Parameters:
*
* state - <mxCellState> whose label should be checked.
* shape - <mxText> shape to be checked.
*/
mxCellRenderer.prototype.isTextShapeInvalid = function(state, shape)
{
function check(property, stylename, defaultValue)
{
var result = false;
// Workaround for spacing added to directional spacing
if (stylename == 'spacingTop' || stylename == 'spacingRight' ||
stylename == 'spacingBottom' || stylename == 'spacingLeft')
{
result = parseFloat(shape[property]) - parseFloat(shape.spacing) !=
(state.style[stylename] || defaultValue);
}
else
{
result = shape[property] != (state.style[stylename] || defaultValue);
}
return result;
};
return check('fontStyle', mxConstants.STYLE_FONTSTYLE, mxConstants.DEFAULT_FONTSTYLE) ||
check('family', mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY) ||
check('size', mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) ||
check('color', mxConstants.STYLE_FONTCOLOR, 'black') ||
check('align', mxConstants.STYLE_ALIGN, '') ||
check('valign', mxConstants.STYLE_VERTICAL_ALIGN, '') ||
check('spacing', mxConstants.STYLE_SPACING, 2) ||
check('spacingTop', mxConstants.STYLE_SPACING_TOP, 0) ||
check('spacingRight', mxConstants.STYLE_SPACING_RIGHT, 0) ||
check('spacingBottom', mxConstants.STYLE_SPACING_BOTTOM, 0) ||
check('spacingLeft', mxConstants.STYLE_SPACING_LEFT, 0) ||
check('horizontal', mxConstants.STYLE_HORIZONTAL, true) ||
check('background', mxConstants.STYLE_LABEL_BACKGROUNDCOLOR) ||
check('border', mxConstants.STYLE_LABEL_BORDERCOLOR) ||
check('opacity', mxConstants.STYLE_TEXT_OPACITY, 100) ||
check('textDirection', mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
};
/**
* Function: redrawLabelShape
*
* Called to invoked redraw on the given text shape.
*
* Parameters:
*
* shape - <mxText> shape to be redrawn.
*/
mxCellRenderer.prototype.redrawLabelShape = function(shape)
{
shape.redraw();
};
/**
* Function: getTextScale
*
* Returns the scaling used for the label of the given state
*
* Parameters:
*
* state - <mxCellState> whose label scale should be returned.
*/
mxCellRenderer.prototype.getTextScale = function(state)
{
return state.view.scale;
};
/**
* Function: getLabelBounds
*
* Returns the bounds to be used to draw the label of the given state.
*
* Parameters:
*
* state - <mxCellState> whose label bounds should be returned.
*/
mxCellRenderer.prototype.getLabelBounds = function(state)
{
var graph = state.view.graph;
var scale = state.view.scale;
var isEdge = graph.getModel().isEdge(state.cell);
var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y);
if (isEdge)
{
var spacing = state.text.getSpacing();
bounds.x += spacing.x * scale;
bounds.y += spacing.y * scale;
var geo = graph.getCellGeometry(state.cell);
if (geo != null)
{
bounds.width = Math.max(0, geo.width * scale);
bounds.height = Math.max(0, geo.height * scale);
}
}
else
{
// Inverts label position
if (state.text.isPaintBoundsInverted())
{
var tmp = bounds.x;
bounds.x = bounds.y;
bounds.y = tmp;
}
bounds.x += state.x;
bounds.y += state.y;
// Minimum of 1 fixes alignment bug in HTML labels
bounds.width = Math.max(1, state.width);
bounds.height = Math.max(1, state.height);
var sc = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE);
if (sc != mxConstants.NONE && sc != '')
{
var s = parseFloat(mxUtils.getValue(state.style, mxConstants.STYLE_STROKEWIDTH, 1)) * scale;
var dx = 1 + Math.floor((s - 1) / 2);
var dh = Math.floor(s + 1);
bounds.x += dx;
bounds.y += dx;
bounds.width -= dh;
bounds.height -= dh;
}
}
if (state.text.isPaintBoundsInverted())
{
// Rotates around center of state
var t = (state.width - state.height) / 2;
bounds.x += t;
bounds.y -= t;
var tmp = bounds.width;
bounds.width = bounds.height;
bounds.height = tmp;
}
// Shape can modify its label bounds
if (state.shape != null)
{
var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
if (hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE)
{
bounds = state.shape.getLabelBounds(bounds);
}
}
// Label width style overrides actual label width
var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
if (lw != null)
{
bounds.width = parseFloat(lw) * scale;
}
if (!isEdge)
{
this.rotateLabelBounds(state, bounds);
}
return bounds;
};
/**
* Function: rotateLabelBounds
*
* Adds the shape rotation to the given label bounds and
* applies the alignment and offsets.
*
* Parameters:
*
* state - <mxCellState> whose label bounds should be rotated.
* bounds - <mxRectangle> the rectangle to be rotated.
*/
mxCellRenderer.prototype.rotateLabelBounds = function(state, bounds)
{
bounds.y -= state.text.margin.y * bounds.height;
bounds.x -= state.text.margin.x * bounds.width;
if (!this.legacySpacing || (state.style[mxConstants.STYLE_OVERFLOW] != 'fill' && state.style[mxConstants.STYLE_OVERFLOW] != 'width'))
{
var s = state.view.scale;
var spacing = state.text.getSpacing();
bounds.x += spacing.x * s;
bounds.y += spacing.y * s;
var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
bounds.width = Math.max(0, bounds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (state.text.spacingLeft * s + state.text.spacingRight * s) : 0));
bounds.height = Math.max(0, bounds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (state.text.spacingTop * s + state.text.spacingBottom * s) : 0));
}
var theta = state.text.getTextRotation();
// Only needed if rotated around another center
if (theta != 0 && state != null && state.view.graph.model.isVertex(state.cell))
{
var cx = state.getCenterX();
var cy = state.getCenterY();
if (bounds.x != cx || bounds.y != cy)
{
var rad = theta * (Math.PI / 180);
var pt = mxUtils.getRotatedPoint(new mxPoint(bounds.x, bounds.y),
Math.cos(rad), Math.sin(rad), new mxPoint(cx, cy));
bounds.x = pt.x;
bounds.y = pt.y;
}
}
};
/**
* Function: redrawCellOverlays
*
* Redraws the overlays for the given cell state.
*
* Parameters:
*
* state - <mxCellState> whose overlays should be redrawn.
*/
mxCellRenderer.prototype.redrawCellOverlays = function(state, forced)
{
this.createCellOverlays(state);
if (state.overlays != null)
{
var rot = mxUtils.mod(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0), 90);
var rad = mxUtils.toRadians(rot);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
state.overlays.visit(function(id, shape)
{
var bounds = shape.overlay.getBounds(state);
if (!state.view.graph.getModel().isEdge(state.cell))
{
if (state.shape != null && rot != 0)
{
var cx = bounds.getCenterX();
var cy = bounds.getCenterY();
var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin,
new mxPoint(state.getCenterX(), state.getCenterY()));
cx = point.x;
cy = point.y;
bounds.x = Math.round(cx - bounds.width / 2);
bounds.y = Math.round(cy - bounds.height / 2);
}
}
if (forced || shape.bounds == null || shape.scale != state.view.scale ||
!shape.bounds.equals(bounds))
{
shape.bounds = bounds;
shape.scale = state.view.scale;
shape.redraw();
}
});
}
};
/**
* Function: redrawControl
*
* Redraws the control for the given cell state.
*
* Parameters:
*
* state - <mxCellState> whose control should be redrawn.
*/
mxCellRenderer.prototype.redrawControl = function(state, forced)
{
var image = state.view.graph.getFoldingImage(state);
if (state.control != null && image != null)
{
var bounds = this.getControlBounds(state, image.width, image.height);
var r = (this.legacyControlPosition) ?
mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0) :
state.shape.getTextRotation();
var s = state.view.scale;
if (forced || state.control.scale != s || !state.control.bounds.equals(bounds) ||
state.control.rotation != r)
{
state.control.rotation = r;
state.control.bounds = bounds;
state.control.scale = s;
state.control.redraw();
}
}
};
/**
* Function: getControlBounds
*
* Returns the bounds to be used to draw the control (folding icon) of the
* given state.
*/
mxCellRenderer.prototype.getControlBounds = function(state, w, h)
{
if (state.control != null)
{
var s = state.view.scale;
var cx = state.getCenterX();
var cy = state.getCenterY();
if (!state.view.graph.getModel().isEdge(state.cell))
{
cx = state.x + w * s;
cy = state.y + h * s;
if (state.shape != null)
{
// TODO: Factor out common code
var rot = state.shape.getShapeRotation();
if (this.legacyControlPosition)
{
rot = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0);
}
else
{
if (state.shape.isPaintBoundsInverted())
{
var t = (state.width - state.height) / 2;
cx += t;
cy -= t;
}
}
if (rot != 0)
{
var rad = mxUtils.toRadians(rot);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin,
new mxPoint(state.getCenterX(), state.getCenterY()));
cx = point.x;
cy = point.y;
}
}
}
return (state.view.graph.getModel().isEdge(state.cell)) ?
new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s))
: new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s));
}
return null;
};
/**
* Function: insertStateAfter
*
* Inserts the given array of <mxShapes> after the given nodes in the DOM.
*
* Parameters:
*
* shapes - Array of <mxShapes> to be inserted.
* node - Node in <drawPane> after which the shapes should be inserted.
* htmlNode - Node in the graph container after which the shapes should be inserted that
* will not go into the <drawPane> (eg. HTML labels without foreignObjects).
*/
mxCellRenderer.prototype.insertStateAfter = function(state, node, htmlNode)
{
var shapes = this.getShapesForState(state);
for (var i = 0; i < shapes.length; i++)
{
if (shapes[i] != null && shapes[i].node != null)
{
var html = shapes[i].node.parentNode != state.view.getDrawPane() &&
shapes[i].node.parentNode != state.view.getOverlayPane();
var temp = (html) ? htmlNode : node;
if (temp != null && temp.nextSibling != shapes[i].node)
{
if (temp.nextSibling == null)
{
temp.parentNode.appendChild(shapes[i].node);
}
else
{
temp.parentNode.insertBefore(shapes[i].node, temp.nextSibling);
}
}
else if (temp == null)
{
// Special case: First HTML node should be first sibling after canvas
if (shapes[i].node.parentNode == state.view.graph.container)
{
var canvas = state.view.canvas;
while (canvas != null && canvas.parentNode != state.view.graph.container)
{
canvas = canvas.parentNode;
}
if (canvas != null && canvas.nextSibling != null)
{
if (canvas.nextSibling != shapes[i].node)
{
shapes[i].node.parentNode.insertBefore(shapes[i].node, canvas.nextSibling);
}
}
else
{
shapes[i].node.parentNode.appendChild(shapes[i].node);
}
}
else if (shapes[i].node.parentNode.firstChild != null && shapes[i].node.parentNode.firstChild != shapes[i].node)
{
// Inserts the node as the first child of the parent to implement the order
shapes[i].node.parentNode.insertBefore(shapes[i].node, shapes[i].node.parentNode.firstChild);
}
}
if (html)
{
htmlNode = shapes[i].node;
}
else
{
node = shapes[i].node;
}
}
}
return [node, htmlNode];
};
/**
* Function: getShapesForState
*
* Returns the <mxShapes> for the given cell state in the order in which they should
* appear in the DOM.
*
* Parameters:
*
* state - <mxCellState> whose shapes should be returned.
*/
mxCellRenderer.prototype.getShapesForState = function(state)
{
return [state.shape, state.text, state.control];
};
/**
* Function: redraw
*
* Updates the bounds or points and scale of the shapes for the given cell
* state. This is called in mxGraphView.validatePoints as the last step of
* updating all cells.
*
* Parameters:
*
* state - <mxCellState> for which the shapes should be updated.
* force - Optional boolean that specifies if the cell should be reconfiured
* and redrawn without any additional checks.
* rendering - Optional boolean that specifies if the cell should actually
* be drawn into the DOM. If this is false then redraw and/or reconfigure
* will not be called on the shape.
*/
mxCellRenderer.prototype.redraw = function(state, force, rendering)
{
var shapeChanged = this.redrawShape(state, force, rendering);
if (state.shape != null && (rendering == null || rendering))
{
this.redrawLabel(state, shapeChanged);
this.redrawCellOverlays(state, shapeChanged);
this.redrawControl(state, shapeChanged);
}
};
/**
* Function: redrawShape
*
* Redraws the shape for the given cell state.
*
* Parameters:
*
* state - <mxCellState> whose label should be redrawn.
*/
mxCellRenderer.prototype.redrawShape = function(state, force, rendering)
{
var model = state.view.graph.model;
var shapeChanged = false;
// Forces creation of new shape if shape style has changed
if (state.shape != null && state.shape.style != null && state.style != null &&
state.shape.style[mxConstants.STYLE_SHAPE] != state.style[mxConstants.STYLE_SHAPE])
{
state.shape.destroy();
state.shape = null;
}
if (state.shape == null && state.view.graph.container != null &&
state.cell != state.view.currentRoot &&
(model.isVertex(state.cell) || model.isEdge(state.cell)))
{
state.shape = this.createShape(state);
if (state.shape != null)
{
state.shape.minSvgStrokeWidth = this.minSvgStrokeWidth;
state.shape.antiAlias = this.antiAlias;
this.createIndicatorShape(state);
this.initializeShape(state);
this.createCellOverlays(state);
this.installListeners(state);
// Forces a refresh of the handler if one exists
state.view.graph.selectionCellsHandler.updateHandler(state);
}
}
else if (!force && state.shape != null && (!mxUtils.equalEntries(state.shape.style,
state.style) || this.checkPlaceholderStyles(state)))
{
state.shape.resetStyles();
this.configureShape(state);
// LATER: Ignore update for realtime to fix reset of current gesture
state.view.graph.selectionCellsHandler.updateHandler(state);
force = true;
}
if (state.shape != null)
{
// Handles changes of the collapse icon
this.createControl(state);
// Redraws the cell if required, ignores changes to bounds if points are
// defined as the bounds are updated for the given points inside the shape
if (force || this.isShapeInvalid(state, state.shape))
{
if (state.absolutePoints != null)
{
state.shape.points = state.absolutePoints.slice();
state.shape.bounds = null;
}
else
{
state.shape.points = null;
state.shape.bounds = new mxRectangle(state.x, state.y, state.width, state.height);
}
state.shape.scale = state.view.scale;
if (rendering == null || rendering)
{
this.doRedrawShape(state);
}
else
{
state.shape.updateBoundingBox();
}
shapeChanged = true;
}
}
return shapeChanged;
};
/**
* Function: doRedrawShape
*
* Invokes redraw on the shape of the given state.
*/
mxCellRenderer.prototype.doRedrawShape = function(state)
{
state.shape.redraw();
};
/**
* Function: isShapeInvalid
*
* Returns true if the given shape must be repainted.
*/
mxCellRenderer.prototype.isShapeInvalid = function(state, shape)
{
return shape.bounds == null || shape.scale != state.view.scale ||
(state.absolutePoints == null && !shape.bounds.equals(state)) ||
(state.absolutePoints != null && !mxUtils.equalPoints(shape.points, state.absolutePoints))
};
/**
* Function: destroy
*
* Destroys the shapes associated with the given cell state.
*
* Parameters:
*
* state - <mxCellState> for which the shapes should be destroyed.
*/
mxCellRenderer.prototype.destroy = function(state)
{
if (state.shape != null)
{
if (state.text != null)
{
state.text.destroy();
state.text = null;
}
if (state.overlays != null)
{
state.overlays.visit(function(id, shape)
{
shape.destroy();
});
state.overlays = null;
}
if (state.control != null)
{
state.control.destroy();
state.control = null;
}
state.shape.destroy();
state.shape = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxEdgeStyle =
{
/**
* Class: mxEdgeStyle
*
* Provides various edge styles to be used as the values for
* <mxConstants.STYLE_EDGE> in a cell style.
*
* Example:
*
* (code)
* var style = stylesheet.getDefaultEdgeStyle();
* style[mxConstants.STYLE_EDGE] = mxEdgeStyle.ElbowConnector;
* (end)
*
* Sets the default edge style to <ElbowConnector>.
*
* Custom edge style:
*
* To write a custom edge style, a function must be added to the mxEdgeStyle
* object as follows:
*
* (code)
* mxEdgeStyle.MyStyle = function(state, source, target, points, result)
* {
* if (source != null && target != null)
* {
* var pt = new mxPoint(target.getCenterX(), source.getCenterY());
*
* if (mxUtils.contains(source, pt.x, pt.y))
* {
* pt.y = source.y + source.height;
* }
*
* result.push(pt);
* }
* };
* (end)
*
* In the above example, a right angle is created using a point on the
* horizontal center of the target vertex and the vertical center of the source
* vertex. The code checks if that point intersects the source vertex and makes
* the edge straight if it does. The point is then added into the result array,
* which acts as the return value of the function.
*
* The new edge style should then be registered in the <mxStyleRegistry> as follows:
* (code)
* mxStyleRegistry.putValue('myEdgeStyle', mxEdgeStyle.MyStyle);
* (end)
*
* The custom edge style above can now be used in a specific edge as follows:
*
* (code)
* model.setStyle(edge, 'edgeStyle=myEdgeStyle');
* (end)
*
* Note that the key of the <mxStyleRegistry> entry for the function should
* be used in string values, unless <mxGraphView.allowEval> is true, in
* which case you can also use mxEdgeStyle.MyStyle for the value in the
* cell style above.
*
* Or it can be used for all edges in the graph as follows:
*
* (code)
* var style = graph.getStylesheet().getDefaultEdgeStyle();
* style[mxConstants.STYLE_EDGE] = mxEdgeStyle.MyStyle;
* (end)
*
* Note that the object can be used directly when programmatically setting
* the value, but the key in the <mxStyleRegistry> should be used when
* setting the value via a key, value pair in a cell style.
*
* Function: EntityRelation
*
* Implements an entity relation style for edges (as used in database
* schema diagrams). At the time the function is called, the result
* array contains a placeholder (null) for the first absolute point,
* that is, the point where the edge and source terminal are connected.
* The implementation of the style then adds all intermediate waypoints
* except for the last point, that is, the connection point between the
* edge and the target terminal. The first ant the last point in the
* result array are then replaced with mxPoints that take into account
* the terminal's perimeter and next point on the edge.
*
* Parameters:
*
* state - <mxCellState> that represents the edge to be updated.
* source - <mxCellState> that represents the source terminal.
* target - <mxCellState> that represents the target terminal.
* points - List of relative control points.
* result - Array of <mxPoints> that represent the actual points of the
* edge.
*/
EntityRelation: function (state, source, target, points, result)
{
var view = state.view;
var graph = view.graph;
var segment = mxUtils.getValue(state.style,
mxConstants.STYLE_SEGMENT,
mxConstants.ENTITY_SEGMENT) * view.scale;
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length-1];
var isSourceLeft = false;
if (p0 != null)
{
source = new mxCellState();
source.x = p0.x;
source.y = p0.y;
}
else if (source != null)
{
var constraint = mxUtils.getPortConstraints(source, state, true, mxConstants.DIRECTION_MASK_NONE);
if (constraint != mxConstants.DIRECTION_MASK_NONE && constraint != mxConstants.DIRECTION_MASK_WEST +
mxConstants.DIRECTION_MASK_EAST)
{
isSourceLeft = constraint == mxConstants.DIRECTION_MASK_WEST;
}
else
{
var sourceGeometry = graph.getCellGeometry(source.cell);
if (sourceGeometry.relative)
{
isSourceLeft = sourceGeometry.x <= 0.5;
}
else if (target != null)
{
isSourceLeft = target.x + target.width < source.x;
}
}
}
else
{
return;
}
var isTargetLeft = true;
if (pe != null)
{
target = new mxCellState();
target.x = pe.x;
target.y = pe.y;
}
else if (target != null)
{
var constraint = mxUtils.getPortConstraints(target, state, false, mxConstants.DIRECTION_MASK_NONE);
if (constraint != mxConstants.DIRECTION_MASK_NONE && constraint != mxConstants.DIRECTION_MASK_WEST +
mxConstants.DIRECTION_MASK_EAST)
{
isTargetLeft = constraint == mxConstants.DIRECTION_MASK_WEST;
}
else
{
var targetGeometry = graph.getCellGeometry(target.cell);
if (targetGeometry.relative)
{
isTargetLeft = targetGeometry.x <= 0.5;
}
else if (source != null)
{
isTargetLeft = source.x + source.width < target.x;
}
}
}
if (source != null && target != null)
{
var x0 = (isSourceLeft) ? source.x : source.x + source.width;
var y0 = view.getRoutingCenterY(source);
var xe = (isTargetLeft) ? target.x : target.x + target.width;
var ye = view.getRoutingCenterY(target);
var seg = segment;
var dx = (isSourceLeft) ? -seg : seg;
var dep = new mxPoint(x0 + dx, y0);
dx = (isTargetLeft) ? -seg : seg;
var arr = new mxPoint(xe + dx, ye);
// Adds intermediate points if both go out on same side
if (isSourceLeft == isTargetLeft)
{
var x = (isSourceLeft) ?
Math.min(x0, xe)-segment :
Math.max(x0, xe)+segment;
result.push(new mxPoint(x, y0));
result.push(new mxPoint(x, ye));
}
else if ((dep.x < arr.x) == isSourceLeft)
{
var midY = y0 + (ye - y0) / 2;
result.push(dep);
result.push(new mxPoint(dep.x, midY));
result.push(new mxPoint(arr.x, midY));
result.push(arr);
}
else
{
result.push(dep);
result.push(arr);
}
}
},
/**
* Function: Loop
*
* Implements a self-reference, aka. loop.
*/
Loop: function (state, source, target, points, result)
{
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length-1];
if (p0 != null && pe != null)
{
if (points != null && points.length > 0)
{
for (var i = 0; i < points.length; i++)
{
var pt = points[i];
pt = state.view.transformControlPoint(state, pt);
result.push(new mxPoint(pt.x, pt.y));
}
}
return;
}
if (source != null)
{
var view = state.view;
var graph = view.graph;
var pt = (points != null && points.length > 0) ? points[0] : null;
if (pt != null)
{
pt = view.transformControlPoint(state, pt);
if (mxUtils.contains(source, pt.x, pt.y))
{
pt = null;
}
}
var x = 0;
var dx = 0;
var y = 0;
var dy = 0;
var seg = mxUtils.getValue(state.style, mxConstants.STYLE_SEGMENT,
graph.gridSize) * view.scale;
var dir = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION,
mxConstants.DIRECTION_WEST);
if (dir == mxConstants.DIRECTION_NORTH ||
dir == mxConstants.DIRECTION_SOUTH)
{
x = view.getRoutingCenterX(source);
dx = seg;
}
else
{
y = view.getRoutingCenterY(source);
dy = seg;
}
if (pt == null ||
pt.x < source.x ||
pt.x > source.x + source.width)
{
if (pt != null)
{
x = pt.x;
dy = Math.max(Math.abs(y - pt.y), dy);
}
else
{
if (dir == mxConstants.DIRECTION_NORTH)
{
y = source.y - 2 * dx;
}
else if (dir == mxConstants.DIRECTION_SOUTH)
{
y = source.y + source.height + 2 * dx;
}
else if (dir == mxConstants.DIRECTION_EAST)
{
x = source.x - 2 * dy;
}
else
{
x = source.x + source.width + 2 * dy;
}
}
}
else if (pt != null)
{
x = view.getRoutingCenterX(source);
dx = Math.max(Math.abs(x - pt.x), dy);
y = pt.y;
dy = 0;
}
result.push(new mxPoint(x - dx, y - dy));
result.push(new mxPoint(x + dx, y + dy));
}
},
/**
* Function: ElbowConnector
*
* Uses either <SideToSide> or <TopToBottom> depending on the horizontal
* flag in the cell style. <SideToSide> is used if horizontal is true or
* unspecified. See <EntityRelation> for a description of the
* parameters.
*/
ElbowConnector: function (state, source, target, points, result)
{
var pt = (points != null && points.length > 0) ? points[0] : null;
var vertical = false;
var horizontal = false;
if (source != null && target != null)
{
if (pt != null)
{
var left = Math.min(source.x, target.x);
var right = Math.max(source.x + source.width,
target.x + target.width);
var top = Math.min(source.y, target.y);
var bottom = Math.max(source.y + source.height,
target.y + target.height);
pt = state.view.transformControlPoint(state, pt);
vertical = pt.y < top || pt.y > bottom;
horizontal = pt.x < left || pt.x > right;
}
else
{
var left = Math.max(source.x, target.x);
var right = Math.min(source.x + source.width,
target.x + target.width);
vertical = left == right;
if (!vertical)
{
var top = Math.max(source.y, target.y);
var bottom = Math.min(source.y + source.height,
target.y + target.height);
horizontal = top == bottom;
}
}
}
if (!horizontal && (vertical ||
state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL))
{
mxEdgeStyle.TopToBottom(state, source, target, points, result);
}
else
{
mxEdgeStyle.SideToSide(state, source, target, points, result);
}
},
/**
* Function: SideToSide
*
* Implements a vertical elbow edge. See <EntityRelation> for a description
* of the parameters.
*/
SideToSide: function (state, source, target, points, result)
{
var view = state.view;
var pt = (points != null && points.length > 0) ? points[0] : null;
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length-1];
if (pt != null)
{
pt = view.transformControlPoint(state, pt);
}
if (p0 != null)
{
source = new mxCellState();
source.x = p0.x;
source.y = p0.y;
}
if (pe != null)
{
target = new mxCellState();
target.x = pe.x;
target.y = pe.y;
}
if (source != null && target != null)
{
var l = Math.max(source.x, target.x);
var r = Math.min(source.x + source.width,
target.x + target.width);
var x = (pt != null) ? pt.x : Math.round(r + (l - r) / 2);
var y1 = view.getRoutingCenterY(source);
var y2 = view.getRoutingCenterY(target);
if (pt != null)
{
if (pt.y >= source.y && pt.y <= source.y + source.height)
{
y1 = pt.y;
}
if (pt.y >= target.y && pt.y <= target.y + target.height)
{
y2 = pt.y;
}
}
if (!mxUtils.contains(target, x, y1) &&
!mxUtils.contains(source, x, y1))
{
result.push(new mxPoint(x, y1));
}
if (!mxUtils.contains(target, x, y2) &&
!mxUtils.contains(source, x, y2))
{
result.push(new mxPoint(x, y2));
}
if (result.length == 1)
{
if (pt != null)
{
if (!mxUtils.contains(target, x, pt.y) &&
!mxUtils.contains(source, x, pt.y))
{
result.push(new mxPoint(x, pt.y));
}
}
else
{
var t = Math.max(source.y, target.y);
var b = Math.min(source.y + source.height,
target.y + target.height);
result.push(new mxPoint(x, t + (b - t) / 2));
}
}
}
},
/**
* Function: TopToBottom
*
* Implements a horizontal elbow edge. See <EntityRelation> for a
* description of the parameters.
*/
TopToBottom: function(state, source, target, points, result)
{
var view = state.view;
var pt = (points != null && points.length > 0) ? points[0] : null;
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length-1];
if (pt != null)
{
pt = view.transformControlPoint(state, pt);
}
if (p0 != null)
{
source = new mxCellState();
source.x = p0.x;
source.y = p0.y;
}
if (pe != null)
{
target = new mxCellState();
target.x = pe.x;
target.y = pe.y;
}
if (source != null && target != null)
{
var t = Math.max(source.y, target.y);
var b = Math.min(source.y + source.height,
target.y + target.height);
var x = view.getRoutingCenterX(source);
if (pt != null &&
pt.x >= source.x &&
pt.x <= source.x + source.width)
{
x = pt.x;
}
var y = (pt != null) ? pt.y : Math.round(b + (t - b) / 2);
if (!mxUtils.contains(target, x, y) &&
!mxUtils.contains(source, x, y))
{
result.push(new mxPoint(x, y));
}
if (pt != null &&
pt.x >= target.x &&
pt.x <= target.x + target.width)
{
x = pt.x;
}
else
{
x = view.getRoutingCenterX(target);
}
if (!mxUtils.contains(target, x, y) &&
!mxUtils.contains(source, x, y))
{
result.push(new mxPoint(x, y));
}
if (result.length == 1)
{
if (pt != null && result.length == 1)
{
if (!mxUtils.contains(target, pt.x, y) &&
!mxUtils.contains(source, pt.x, y))
{
result.push(new mxPoint(pt.x, y));
}
}
else
{
var l = Math.max(source.x, target.x);
var r = Math.min(source.x + source.width,
target.x + target.width);
result.push(new mxPoint(l + (r - l) / 2, y));
}
}
}
},
/**
* Function: SegmentConnector
*
* Implements an orthogonal edge style. Use <mxEdgeSegmentHandler>
* as an interactive handler for this style.
*/
SegmentConnector: function(state, source, target, hints, result)
{
// Creates array of all way- and terminalpoints
var pts = state.absolutePoints;
var tol = Math.max(1, state.view.scale);
// Whether the first segment outgoing from the source end is horizontal
var lastPushed = (result.length > 0) ? result[0] : null;
var horizontal = true;
var hint = null;
// Adds waypoints only if outside of tolerance
function pushPoint(pt)
{
if (lastPushed == null || Math.abs(lastPushed.x - pt.x) >= tol || Math.abs(lastPushed.y - pt.y) >= tol)
{
result.push(pt);
lastPushed = pt;
}
return lastPushed;
};
// Adds the first point
var pt = pts[0];
if (pt == null && source != null)
{
pt = new mxPoint(state.view.getRoutingCenterX(source), state.view.getRoutingCenterY(source));
}
else if (pt != null)
{
pt = pt.clone();
}
pt.x = Math.round(pt.x);
pt.y = Math.round(pt.y);
var lastInx = pts.length - 1;
// Adds the waypoints
if (hints != null && hints.length > 0)
{
// Converts all hints and removes nulls
var newHints = [];
for (var i = 0; i < hints.length; i++)
{
var tmp = state.view.transformControlPoint(state, hints[i]);
if (tmp != null)
{
tmp.x = Math.round(tmp.x);
tmp.y = Math.round(tmp.y);
newHints.push(tmp);
}
}
if (newHints.length == 0)
{
return;
}
hints = newHints;
// Aligns source and target hint to fixed points
if (pt != null && hints[0] != null)
{
if (Math.abs(hints[0].x - pt.x) < tol)
{
hints[0].x = pt.x;
}
if (Math.abs(hints[0].y - pt.y) < tol)
{
hints[0].y = pt.y;
}
}
var pe = pts[lastInx];
if (pe != null && hints[hints.length - 1] != null)
{
if (Math.abs(hints[hints.length - 1].x - pe.x) < tol)
{
hints[hints.length - 1].x = pe.x;
}
if (Math.abs(hints[hints.length - 1].y - pe.y) < tol)
{
hints[hints.length - 1].y = pe.y;
}
}
hint = hints[0];
var currentTerm = source;
var currentPt = pts[0];
var hozChan = false;
var vertChan = false;
var currentHint = hint;
if (currentPt != null)
{
currentPt.x = Math.round(currentPt.x);
currentPt.y = Math.round(currentPt.y);
currentTerm = null;
}
// Check for alignment with fixed points and with channels
// at source and target segments only
for (var i = 0; i < 2; i++)
{
var fixedVertAlign = currentPt != null && currentPt.x == currentHint.x;
var fixedHozAlign = currentPt != null && currentPt.y == currentHint.y;
var inHozChan = currentTerm != null && (currentHint.y >= currentTerm.y &&
currentHint.y <= currentTerm.y + currentTerm.height);
var inVertChan = currentTerm != null && (currentHint.x >= currentTerm.x &&
currentHint.x <= currentTerm.x + currentTerm.width);
hozChan = fixedHozAlign || (currentPt == null && inHozChan);
vertChan = fixedVertAlign || (currentPt == null && inVertChan);
// If the current hint falls in both the hor and vert channels in the case
// of a floating port, or if the hint is exactly co-incident with a
// fixed point, ignore the source and try to work out the orientation
// from the target end
if (i==0 && ((hozChan && vertChan) || (fixedVertAlign && fixedHozAlign)))
{
}
else
{
if (currentPt != null && (!fixedHozAlign && !fixedVertAlign) && (inHozChan || inVertChan))
{
horizontal = inHozChan ? false : true;
break;
}
if (vertChan || hozChan)
{
horizontal = hozChan;
if (i == 1)
{
// Work back from target end
horizontal = hints.length % 2 == 0 ? hozChan : vertChan;
}
break;
}
}
currentTerm = target;
currentPt = pts[lastInx];
if (currentPt != null)
{
currentPt.x = Math.round(currentPt.x);
currentPt.y = Math.round(currentPt.y);
currentTerm = null;
}
currentHint = hints[hints.length - 1];
if (fixedVertAlign && fixedHozAlign)
{
hints = hints.slice(1);
}
}
if (horizontal && ((pts[0] != null && pts[0].y != hint.y) ||
(pts[0] == null && source != null &&
(hint.y < source.y || hint.y > source.y + source.height))))
{
pushPoint(new mxPoint(pt.x, hint.y));
}
else if (!horizontal && ((pts[0] != null && pts[0].x != hint.x) ||
(pts[0] == null && source != null &&
(hint.x < source.x || hint.x > source.x + source.width))))
{
pushPoint(new mxPoint(hint.x, pt.y));
}
if (horizontal)
{
pt.y = hint.y;
}
else
{
pt.x = hint.x;
}
for (var i = 0; i < hints.length; i++)
{
horizontal = !horizontal;
hint = hints[i];
// mxLog.show();
// mxLog.debug('hint', i, hint.x, hint.y);
if (horizontal)
{
pt.y = hint.y;
}
else
{
pt.x = hint.x;
}
pushPoint(pt.clone());
}
}
else
{
hint = pt;
// FIXME: First click in connect preview toggles orientation
horizontal = true;
}
// Adds the last point
pt = pts[lastInx];
if (pt == null && target != null)
{
pt = new mxPoint(state.view.getRoutingCenterX(target), state.view.getRoutingCenterY(target));
}
if (pt != null)
{
pt.x = Math.round(pt.x);
pt.y = Math.round(pt.y);
if (hint != null)
{
if (horizontal && ((pts[lastInx] != null && pts[lastInx].y != hint.y) ||
(pts[lastInx] == null && target != null &&
(hint.y < target.y || hint.y > target.y + target.height))))
{
pushPoint(new mxPoint(pt.x, hint.y));
}
else if (!horizontal && ((pts[lastInx] != null && pts[lastInx].x != hint.x) ||
(pts[lastInx] == null && target != null &&
(hint.x < target.x || hint.x > target.x + target.width))))
{
pushPoint(new mxPoint(hint.x, pt.y));
}
}
}
// Removes bends inside the source terminal for floating ports
if (pts[0] == null && source != null)
{
while (result.length > 1 && result[1] != null &&
mxUtils.contains(source, result[1].x, result[1].y))
{
result.splice(1, 1);
}
}
// Removes bends inside the target terminal
if (pts[lastInx] == null && target != null)
{
while (result.length > 1 && result[result.length - 1] != null &&
mxUtils.contains(target, result[result.length - 1].x, result[result.length - 1].y))
{
result.splice(result.length - 1, 1);
}
}
// Removes last point if inside tolerance with end point
if (pe != null && result[result.length - 1] != null &&
Math.abs(pe.x - result[result.length - 1].x) <= tol &&
Math.abs(pe.y - result[result.length - 1].y) <= tol)
{
result.splice(result.length - 1, 1);
// Lines up second last point in result with end point
if (result[result.length - 1] != null)
{
if (Math.abs(result[result.length - 1].x - pe.x) < tol)
{
result[result.length - 1].x = pe.x;
}
if (Math.abs(result[result.length - 1].y - pe.y) < tol)
{
result[result.length - 1].y = pe.y;
}
}
}
},
orthBuffer: 10,
orthPointsFallback: true,
dirVectors: [ [ -1, 0 ],
[ 0, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 0 ], [ 0, -1 ], [ 1, 0 ] ],
wayPoints1: [ [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0],
[ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0] ],
routePatterns: [
[ [ 513, 2308, 2081, 2562 ], [ 513, 1090, 514, 2184, 2114, 2561 ],
[ 513, 1090, 514, 2564, 2184, 2562 ],
[ 513, 2308, 2561, 1090, 514, 2568, 2308 ] ],
[ [ 514, 1057, 513, 2308, 2081, 2562 ], [ 514, 2184, 2114, 2561 ],
[ 514, 2184, 2562, 1057, 513, 2564, 2184 ],
[ 514, 1057, 513, 2568, 2308, 2561 ] ],
[ [ 1090, 514, 1057, 513, 2308, 2081, 2562 ], [ 2114, 2561 ],
[ 1090, 2562, 1057, 513, 2564, 2184 ],
[ 1090, 514, 1057, 513, 2308, 2561, 2568 ] ],
[ [ 2081, 2562 ], [ 1057, 513, 1090, 514, 2184, 2114, 2561 ],
[ 1057, 513, 1090, 514, 2184, 2562, 2564 ],
[ 1057, 2561, 1090, 514, 2568, 2308 ] ] ],
inlineRoutePatterns: [
[ null, [ 2114, 2568 ], null, null ],
[ null, [ 514, 2081, 2114, 2568 ] , null, null ],
[ null, [ 2114, 2561 ], null, null ],
[ [ 2081, 2562 ], [ 1057, 2114, 2568 ],
[ 2184, 2562 ],
null ] ],
vertexSeperations: [],
limits: [
[ 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ],
LEFT_MASK: 32,
TOP_MASK: 64,
RIGHT_MASK: 128,
BOTTOM_MASK: 256,
LEFT: 1,
TOP: 2,
RIGHT: 4,
BOTTOM: 8,
// TODO remove magic numbers
SIDE_MASK: 480,
//mxEdgeStyle.LEFT_MASK | mxEdgeStyle.TOP_MASK | mxEdgeStyle.RIGHT_MASK
//| mxEdgeStyle.BOTTOM_MASK,
CENTER_MASK: 512,
SOURCE_MASK: 1024,
TARGET_MASK: 2048,
VERTEX_MASK: 3072,
// mxEdgeStyle.SOURCE_MASK | mxEdgeStyle.TARGET_MASK,
getJettySize: function(state, source, target, points, isSource)
{
var value = mxUtils.getValue(state.style, (isSource) ? mxConstants.STYLE_SOURCE_JETTY_SIZE :
mxConstants.STYLE_TARGET_JETTY_SIZE, mxUtils.getValue(state.style,
mxConstants.STYLE_JETTY_SIZE, mxEdgeStyle.orthBuffer));
if (value == 'auto')
{
// Computes the automatic jetty size
var type = mxUtils.getValue(state.style, (isSource) ? mxConstants.STYLE_STARTARROW : mxConstants.STYLE_ENDARROW, mxConstants.NONE);
if (type != mxConstants.NONE)
{
var size = mxUtils.getNumber(state.style, (isSource) ? mxConstants.STYLE_STARTSIZE : mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE);
value = Math.max(2, Math.ceil((size + mxEdgeStyle.orthBuffer) / mxEdgeStyle.orthBuffer)) * mxEdgeStyle.orthBuffer;
}
else
{
value = 2 * mxEdgeStyle.orthBuffer;
}
}
return value;
},
/**
* Function: OrthConnector
*
* Implements a local orthogonal router between the given
* cells.
*
* Parameters:
*
* state - <mxCellState> that represents the edge to be updated.
* source - <mxCellState> that represents the source terminal.
* target - <mxCellState> that represents the target terminal.
* points - List of relative control points.
* result - Array of <mxPoints> that represent the actual points of the
* edge.
*
*/
OrthConnector: function(state, source, target, points, result)
{
var graph = state.view.graph;
var sourceEdge = source == null ? false : graph.getModel().isEdge(source.cell);
var targetEdge = target == null ? false : graph.getModel().isEdge(target.cell);
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length-1];
var sourceX = source != null ? source.x : p0.x;
var sourceY = source != null ? source.y : p0.y;
var sourceWidth = source != null ? source.width : 0;
var sourceHeight = source != null ? source.height : 0;
var targetX = target != null ? target.x : pe.x;
var targetY = target != null ? target.y : pe.y;
var targetWidth = target != null ? target.width : 0;
var targetHeight = target != null ? target.height : 0;
var scaledSourceBuffer = state.view.scale * mxEdgeStyle.getJettySize(state, source, target, points, true);
var scaledTargetBuffer = state.view.scale * mxEdgeStyle.getJettySize(state, source, target, points, false);
// Workaround for loop routing within buffer zone
if (source != null && target == source)
{
scaledTargetBuffer = Math.max(scaledSourceBuffer, scaledTargetBuffer);
scaledSourceBuffer = scaledTargetBuffer;
}
var totalBuffer = scaledTargetBuffer + scaledSourceBuffer;
var tooShort = false;
// Checks minimum distance for fixed points and falls back to segment connector
if (p0 != null && pe != null)
{
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
tooShort = dx * dx + dy * dy < totalBuffer * totalBuffer;
}
if (tooShort || (mxEdgeStyle.orthPointsFallback && (points != null &&
points.length > 0)) || sourceEdge || targetEdge)
{
mxEdgeStyle.SegmentConnector(state, source, target, points, result);
return;
}
// Determine the side(s) of the source and target vertices
// that the edge may connect to
// portConstraint [source, target]
var portConstraint = [mxConstants.DIRECTION_MASK_ALL, mxConstants.DIRECTION_MASK_ALL];
var rotation = 0;
if (source != null)
{
portConstraint[0] = mxUtils.getPortConstraints(source, state, true,
mxConstants.DIRECTION_MASK_ALL);
rotation = mxUtils.getValue(source.style, mxConstants.STYLE_ROTATION, 0);
if (rotation != 0)
{
var newRect = mxUtils.getBoundingBox(new mxRectangle(sourceX, sourceY, sourceWidth, sourceHeight), rotation);
sourceX = newRect.x;
sourceY = newRect.y;
sourceWidth = newRect.width;
sourceHeight = newRect.height;
}
}
if (target != null)
{
portConstraint[1] = mxUtils.getPortConstraints(target, state, false,
mxConstants.DIRECTION_MASK_ALL);
rotation = mxUtils.getValue(target.style, mxConstants.STYLE_ROTATION, 0);
if (rotation != 0)
{
var newRect = mxUtils.getBoundingBox(new mxRectangle(targetX, targetY, targetWidth, targetHeight), rotation);
targetX = newRect.x;
targetY = newRect.y;
targetWidth = newRect.width;
targetHeight = newRect.height;
}
}
// Avoids floating point number errors
sourceX = Math.round(sourceX * 10) / 10;
sourceY = Math.round(sourceY * 10) / 10;
sourceWidth = Math.round(sourceWidth * 10) / 10;
sourceHeight = Math.round(sourceHeight * 10) / 10;
targetX = Math.round(targetX * 10) / 10;
targetY = Math.round(targetY * 10) / 10;
targetWidth = Math.round(targetWidth * 10) / 10;
targetHeight = Math.round(targetHeight * 10) / 10;
var dir = [0, 0];
// Work out which faces of the vertices present against each other
// in a way that would allow a 3-segment connection if port constraints
// permitted.
// geo -> [source, target] [x, y, width, height]
var geo = [ [sourceX, sourceY, sourceWidth, sourceHeight] ,
[targetX, targetY, targetWidth, targetHeight] ];
var buffer = [scaledSourceBuffer, scaledTargetBuffer];
for (var i = 0; i < 2; i++)
{
mxEdgeStyle.limits[i][1] = geo[i][0] - buffer[i];
mxEdgeStyle.limits[i][2] = geo[i][1] - buffer[i];
mxEdgeStyle.limits[i][4] = geo[i][0] + geo[i][2] + buffer[i];
mxEdgeStyle.limits[i][8] = geo[i][1] + geo[i][3] + buffer[i];
}
// Work out which quad the target is in
var sourceCenX = geo[0][0] + geo[0][2] / 2.0;
var sourceCenY = geo[0][1] + geo[0][3] / 2.0;
var targetCenX = geo[1][0] + geo[1][2] / 2.0;
var targetCenY = geo[1][1] + geo[1][3] / 2.0;
var dx = sourceCenX - targetCenX;
var dy = sourceCenY - targetCenY;
var quad = 0;
if (dx < 0)
{
if (dy < 0)
{
quad = 2;
}
else
{
quad = 1;
}
}
else
{
if (dy <= 0)
{
quad = 3;
// Special case on x = 0 and negative y
if (dx == 0)
{
quad = 2;
}
}
}
// Check for connection constraints
var currentTerm = null;
if (source != null)
{
currentTerm = p0;
}
var constraint = [ [0.5, 0.5] , [0.5, 0.5] ];
for (var i = 0; i < 2; i++)
{
if (currentTerm != null)
{
constraint[i][0] = (currentTerm.x - geo[i][0]) / geo[i][2];
if (Math.abs(currentTerm.x - geo[i][0]) <= 1)
{
dir[i] = mxConstants.DIRECTION_MASK_WEST;
}
else if (Math.abs(currentTerm.x - geo[i][0] - geo[i][2]) <= 1)
{
dir[i] = mxConstants.DIRECTION_MASK_EAST;
}
constraint[i][1] = (currentTerm.y - geo[i][1]) / geo[i][3];
if (Math.abs(currentTerm.y - geo[i][1]) <= 1)
{
dir[i] = mxConstants.DIRECTION_MASK_NORTH;
}
else if (Math.abs(currentTerm.y - geo[i][1] - geo[i][3]) <= 1)
{
dir[i] = mxConstants.DIRECTION_MASK_SOUTH;
}
}
currentTerm = null;
if (target != null)
{
currentTerm = pe;
}
}
var sourceTopDist = geo[0][1] - (geo[1][1] + geo[1][3]);
var sourceLeftDist = geo[0][0] - (geo[1][0] + geo[1][2]);
var sourceBottomDist = geo[1][1] - (geo[0][1] + geo[0][3]);
var sourceRightDist = geo[1][0] - (geo[0][0] + geo[0][2]);
mxEdgeStyle.vertexSeperations[1] = Math.max(sourceLeftDist - totalBuffer, 0);
mxEdgeStyle.vertexSeperations[2] = Math.max(sourceTopDist - totalBuffer, 0);
mxEdgeStyle.vertexSeperations[4] = Math.max(sourceBottomDist - totalBuffer, 0);
mxEdgeStyle.vertexSeperations[3] = Math.max(sourceRightDist - totalBuffer, 0);
//==============================================================
// Start of source and target direction determination
// Work through the preferred orientations by relative positioning
// of the vertices and list them in preferred and available order
var dirPref = [];
var horPref = [];
var vertPref = [];
horPref[0] = (sourceLeftDist >= sourceRightDist) ? mxConstants.DIRECTION_MASK_WEST
: mxConstants.DIRECTION_MASK_EAST;
vertPref[0] = (sourceTopDist >= sourceBottomDist) ? mxConstants.DIRECTION_MASK_NORTH
: mxConstants.DIRECTION_MASK_SOUTH;
horPref[1] = mxUtils.reversePortConstraints(horPref[0]);
vertPref[1] = mxUtils.reversePortConstraints(vertPref[0]);
var preferredHorizDist = sourceLeftDist >= sourceRightDist ? sourceLeftDist
: sourceRightDist;
var preferredVertDist = sourceTopDist >= sourceBottomDist ? sourceTopDist
: sourceBottomDist;
var prefOrdering = [ [0, 0] , [0, 0] ];
var preferredOrderSet = false;
// If the preferred port isn't available, switch it
for (var i = 0; i < 2; i++)
{
if (dir[i] != 0x0)
{
continue;
}
if ((horPref[i] & portConstraint[i]) == 0)
{
horPref[i] = mxUtils.reversePortConstraints(horPref[i]);
}
if ((vertPref[i] & portConstraint[i]) == 0)
{
vertPref[i] = mxUtils
.reversePortConstraints(vertPref[i]);
}
prefOrdering[i][0] = vertPref[i];
prefOrdering[i][1] = horPref[i];
}
if (preferredVertDist > 0
&& preferredHorizDist > 0)
{
// Possibility of two segment edge connection
if (((horPref[0] & portConstraint[0]) > 0)
&& ((vertPref[1] & portConstraint[1]) > 0))
{
prefOrdering[0][0] = horPref[0];
prefOrdering[0][1] = vertPref[0];
prefOrdering[1][0] = vertPref[1];
prefOrdering[1][1] = horPref[1];
preferredOrderSet = true;
}
else if (((vertPref[0] & portConstraint[0]) > 0)
&& ((horPref[1] & portConstraint[1]) > 0))
{
prefOrdering[0][0] = vertPref[0];
prefOrdering[0][1] = horPref[0];
prefOrdering[1][0] = horPref[1];
prefOrdering[1][1] = vertPref[1];
preferredOrderSet = true;
}
}
if (preferredVertDist > 0 && !preferredOrderSet)
{
prefOrdering[0][0] = vertPref[0];
prefOrdering[0][1] = horPref[0];
prefOrdering[1][0] = vertPref[1];
prefOrdering[1][1] = horPref[1];
preferredOrderSet = true;
}
if (preferredHorizDist > 0 && !preferredOrderSet)
{
prefOrdering[0][0] = horPref[0];
prefOrdering[0][1] = vertPref[0];
prefOrdering[1][0] = horPref[1];
prefOrdering[1][1] = vertPref[1];
preferredOrderSet = true;
}
// The source and target prefs are now an ordered list of
// the preferred port selections
// If the list contains gaps, compact it
for (var i = 0; i < 2; i++)
{
if (dir[i] != 0x0)
{
continue;
}
if ((prefOrdering[i][0] & portConstraint[i]) == 0)
{
prefOrdering[i][0] = prefOrdering[i][1];
}
dirPref[i] = prefOrdering[i][0] & portConstraint[i];
dirPref[i] |= (prefOrdering[i][1] & portConstraint[i]) << 8;
dirPref[i] |= (prefOrdering[1 - i][i] & portConstraint[i]) << 16;
dirPref[i] |= (prefOrdering[1 - i][1 - i] & portConstraint[i]) << 24;
if ((dirPref[i] & 0xF) == 0)
{
dirPref[i] = dirPref[i] << 8;
}
if ((dirPref[i] & 0xF00) == 0)
{
dirPref[i] = (dirPref[i] & 0xF) | dirPref[i] >> 8;
}
if ((dirPref[i] & 0xF0000) == 0)
{
dirPref[i] = (dirPref[i] & 0xFFFF)
| ((dirPref[i] & 0xF000000) >> 8);
}
dir[i] = dirPref[i] & 0xF;
if (portConstraint[i] == mxConstants.DIRECTION_MASK_WEST
|| portConstraint[i] == mxConstants.DIRECTION_MASK_NORTH
|| portConstraint[i] == mxConstants.DIRECTION_MASK_EAST
|| portConstraint[i] == mxConstants.DIRECTION_MASK_SOUTH)
{
dir[i] = portConstraint[i];
}
}
//==============================================================
// End of source and target direction determination
var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3
: dir[0];
var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3
: dir[1];
sourceIndex -= quad;
targetIndex -= quad;
if (sourceIndex < 1)
{
sourceIndex += 4;
}
if (targetIndex < 1)
{
targetIndex += 4;
}
var routePattern = mxEdgeStyle.routePatterns[sourceIndex - 1][targetIndex - 1];
mxEdgeStyle.wayPoints1[0][0] = geo[0][0];
mxEdgeStyle.wayPoints1[0][1] = geo[0][1];
switch (dir[0])
{
case mxConstants.DIRECTION_MASK_WEST:
mxEdgeStyle.wayPoints1[0][0] -= scaledSourceBuffer;
mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3];
break;
case mxConstants.DIRECTION_MASK_SOUTH:
mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2];
mxEdgeStyle.wayPoints1[0][1] += geo[0][3] + scaledSourceBuffer;
break;
case mxConstants.DIRECTION_MASK_EAST:
mxEdgeStyle.wayPoints1[0][0] += geo[0][2] + scaledSourceBuffer;
mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3];
break;
case mxConstants.DIRECTION_MASK_NORTH:
mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2];
mxEdgeStyle.wayPoints1[0][1] -= scaledSourceBuffer;
break;
}
var currentIndex = 0;
// Orientation, 0 horizontal, 1 vertical
var lastOrientation = (dir[0] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0
: 1;
var initialOrientation = lastOrientation;
var currentOrientation = 0;
for (var i = 0; i < routePattern.length; i++)
{
var nextDirection = routePattern[i] & 0xF;
// Rotate the index of this direction by the quad
// to get the real direction
var directionIndex = nextDirection == mxConstants.DIRECTION_MASK_EAST ? 3
: nextDirection;
directionIndex += quad;
if (directionIndex > 4)
{
directionIndex -= 4;
}
var direction = mxEdgeStyle.dirVectors[directionIndex - 1];
currentOrientation = (directionIndex % 2 > 0) ? 0 : 1;
// Only update the current index if the point moved
// in the direction of the current segment move,
// otherwise the same point is moved until there is
// a segment direction change
if (currentOrientation != lastOrientation)
{
currentIndex++;
// Copy the previous way point into the new one
// We can't base the new position on index - 1
// because sometime elbows turn out not to exist,
// then we'd have to rewind.
mxEdgeStyle.wayPoints1[currentIndex][0] = mxEdgeStyle.wayPoints1[currentIndex - 1][0];
mxEdgeStyle.wayPoints1[currentIndex][1] = mxEdgeStyle.wayPoints1[currentIndex - 1][1];
}
var tar = (routePattern[i] & mxEdgeStyle.TARGET_MASK) > 0;
var sou = (routePattern[i] & mxEdgeStyle.SOURCE_MASK) > 0;
var side = (routePattern[i] & mxEdgeStyle.SIDE_MASK) >> 5;
side = side << quad;
if (side > 0xF)
{
side = side >> 4;
}
var center = (routePattern[i] & mxEdgeStyle.CENTER_MASK) > 0;
if ((sou || tar) && side < 9)
{
var limit = 0;
var souTar = sou ? 0 : 1;
if (center && currentOrientation == 0)
{
limit = geo[souTar][0] + constraint[souTar][0] * geo[souTar][2];
}
else if (center)
{
limit = geo[souTar][1] + constraint[souTar][1] * geo[souTar][3];
}
else
{
limit = mxEdgeStyle.limits[souTar][side];
}
if (currentOrientation == 0)
{
var lastX = mxEdgeStyle.wayPoints1[currentIndex][0];
var deltaX = (limit - lastX) * direction[0];
if (deltaX > 0)
{
mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0]
* deltaX;
}
}
else
{
var lastY = mxEdgeStyle.wayPoints1[currentIndex][1];
var deltaY = (limit - lastY) * direction[1];
if (deltaY > 0)
{
mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1]
* deltaY;
}
}
}
else if (center)
{
// Which center we're travelling to depend on the current direction
mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0]
* Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2);
mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1]
* Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2);
}
if (currentIndex > 0
&& mxEdgeStyle.wayPoints1[currentIndex][currentOrientation] == mxEdgeStyle.wayPoints1[currentIndex - 1][currentOrientation])
{
currentIndex--;
}
else
{
lastOrientation = currentOrientation;
}
}
for (var i = 0; i <= currentIndex; i++)
{
if (i == currentIndex)
{
// Last point can cause last segment to be in
// same direction as jetty/approach. If so,
// check the number of points is consistent
// with the relative orientation of source and target
// jx. Same orientation requires an even
// number of turns (points), different requires
// odd.
var targetOrientation = (dir[1] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0
: 1;
var sameOrient = targetOrientation == initialOrientation ? 0 : 1;
// (currentIndex + 1) % 2 is 0 for even number of points,
// 1 for odd
if (sameOrient != (currentIndex + 1) % 2)
{
// The last point isn't required
break;
}
}
result.push(new mxPoint(Math.round(mxEdgeStyle.wayPoints1[i][0]), Math.round(mxEdgeStyle.wayPoints1[i][1])));
}
// Removes duplicates
var index = 1;
while (index < result.length)
{
if (result[index - 1] == null || result[index] == null ||
result[index - 1].x != result[index].x ||
result[index - 1].y != result[index].y)
{
index++;
}
else
{
result.splice(index, 1);
}
}
},
getRoutePattern: function(dir, quad, dx, dy)
{
var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3
: dir[0];
var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3
: dir[1];
sourceIndex -= quad;
targetIndex -= quad;
if (sourceIndex < 1)
{
sourceIndex += 4;
}
if (targetIndex < 1)
{
targetIndex += 4;
}
var result = routePatterns[sourceIndex - 1][targetIndex - 1];
if (dx == 0 || dy == 0)
{
if (inlineRoutePatterns[sourceIndex - 1][targetIndex - 1] != null)
{
result = inlineRoutePatterns[sourceIndex - 1][targetIndex - 1];
}
}
return result;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxStyleRegistry =
{
/**
* Class: mxStyleRegistry
*
* Singleton class that acts as a global converter from string to object values
* in a style. This is currently only used to perimeters and edge styles.
*
* Variable: values
*
* Maps from strings to objects.
*/
values: [],
/**
* Function: putValue
*
* Puts the given object into the registry under the given name.
*/
putValue: function(name, obj)
{
mxStyleRegistry.values[name] = obj;
},
/**
* Function: getValue
*
* Returns the value associated with the given name.
*/
getValue: function(name)
{
return mxStyleRegistry.values[name];
},
/**
* Function: getName
*
* Returns the name for the given value.
*/
getName: function(value)
{
for (var key in mxStyleRegistry.values)
{
if (mxStyleRegistry.values[key] == value)
{
return key;
}
}
return null;
}
};
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ELBOW, mxEdgeStyle.ElbowConnector);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ENTITY_RELATION, mxEdgeStyle.EntityRelation);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_LOOP, mxEdgeStyle.Loop);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SIDETOSIDE, mxEdgeStyle.SideToSide);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_TOPTOBOTTOM, mxEdgeStyle.TopToBottom);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ORTHOGONAL, mxEdgeStyle.OrthConnector);
mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SEGMENT, mxEdgeStyle.SegmentConnector);
mxStyleRegistry.putValue(mxConstants.PERIMETER_ELLIPSE, mxPerimeter.EllipsePerimeter);
mxStyleRegistry.putValue(mxConstants.PERIMETER_RECTANGLE, mxPerimeter.RectanglePerimeter);
mxStyleRegistry.putValue(mxConstants.PERIMETER_RHOMBUS, mxPerimeter.RhombusPerimeter);
mxStyleRegistry.putValue(mxConstants.PERIMETER_TRIANGLE, mxPerimeter.TrianglePerimeter);
mxStyleRegistry.putValue(mxConstants.PERIMETER_HEXAGON, mxPerimeter.HexagonPerimeter);
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphView
*
* Extends <mxEventSource> to implement a view for a graph. This class is in
* charge of computing the absolute coordinates for the relative child
* geometries, the points for perimeters and edge styles and keeping them
* cached in <mxCellStates> for faster retrieval. The states are updated
* whenever the model or the view state (translate, scale) changes. The scale
* and translate are honoured in the bounds.
*
* Event: mxEvent.UNDO
*
* Fires after the root was changed in <setCurrentRoot>. The <code>edit</code>
* property contains the <mxUndoableEdit> which contains the
* <mxCurrentRootChange>.
*
* Event: mxEvent.SCALE_AND_TRANSLATE
*
* Fires after the scale and translate have been changed in <scaleAndTranslate>.
* The <code>scale</code>, <code>previousScale</code>, <code>translate</code>
* and <code>previousTranslate</code> properties contain the new and previous
* scale and translate, respectively.
*
* Event: mxEvent.SCALE
*
* Fires after the scale was changed in <setScale>. The <code>scale</code> and
* <code>previousScale</code> properties contain the new and previous scale.
*
* Event: mxEvent.TRANSLATE
*
* Fires after the translate was changed in <setTranslate>. The
* <code>translate</code> and <code>previousTranslate</code> properties contain
* the new and previous value for translate.
*
* Event: mxEvent.DOWN and mxEvent.UP
*
* Fire if the current root is changed by executing an <mxCurrentRootChange>.
* The event name depends on the location of the root in the cell hierarchy
* with respect to the current root. The <code>root</code> and
* <code>previous</code> properties contain the new and previous root,
* respectively.
*
* Constructor: mxGraphView
*
* Constructs a new view for the given <mxGraph>.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxGraphView(graph)
{
this.graph = graph;
this.translate = new mxPoint();
this.graphBounds = new mxRectangle();
this.states = new mxDictionary();
};
/**
* Extends mxEventSource.
*/
mxGraphView.prototype = new mxEventSource();
mxGraphView.prototype.constructor = mxGraphView;
/**
*
*/
mxGraphView.prototype.EMPTY_POINT = new mxPoint();
/**
* Variable: doneResource
*
* Specifies the resource key for the status message after a long operation.
* If the resource for this key does not exist then the value is used as
* the status message. Default is 'done'.
*/
mxGraphView.prototype.doneResource = (mxClient.language != 'none') ? 'done' : '';
/**
* Function: updatingDocumentResource
*
* Specifies the resource key for the status message while the document is
* being updated. If the resource for this key does not exist then the
* value is used as the status message. Default is 'updatingDocument'.
*/
mxGraphView.prototype.updatingDocumentResource = (mxClient.language != 'none') ? 'updatingDocument' : '';
/**
* Variable: allowEval
*
* Specifies if string values in cell styles should be evaluated using
* <mxUtils.eval>. This will only be used if the string values can't be mapped
* to objects using <mxStyleRegistry>. Default is false. NOTE: Enabling this
* switch carries a possible security risk.
*/
mxGraphView.prototype.allowEval = false;
/**
* Variable: captureDocumentGesture
*
* Specifies if a gesture should be captured when it goes outside of the
* graph container. Default is true.
*/
mxGraphView.prototype.captureDocumentGesture = true;
/**
* Variable: optimizeVmlReflows
*
* Specifies if the <canvas> should be hidden while rendering in IE8 standards
* mode and quirks mode. This will significantly improve rendering performance.
* Default is true.
*/
mxGraphView.prototype.optimizeVmlReflows = true;
/**
* Variable: rendering
*
* Specifies if shapes should be created, updated and destroyed using the
* methods of <mxCellRenderer> in <graph>. Default is true.
*/
mxGraphView.prototype.rendering = true;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxGraphView.prototype.graph = null;
/**
* Variable: currentRoot
*
* <mxCell> that acts as the root of the displayed cell hierarchy.
*/
mxGraphView.prototype.currentRoot = null;
/**
* Variable: graphBounds
*
* <mxRectangle> that caches the scales, translated bounds of the current view.
*/
mxGraphView.prototype.graphBounds = null;
/**
* Variable: scale
*
* Specifies the scale. Default is 1 (100%).
*/
mxGraphView.prototype.scale = 1;
/**
* Variable: translate
*
* <mxPoint> that specifies the current translation. Default is a new
* empty <mxPoint>.
*/
mxGraphView.prototype.translate = null;
/**
* Variable: states
*
* <mxDictionary> that maps from cell IDs to <mxCellStates>.
*/
mxGraphView.prototype.states = null;
/**
* Variable: updateStyle
*
* Specifies if the style should be updated in each validation step. If this
* is false then the style is only updated if the state is created or if the
* style of the cell was changed. Default is false.
*/
mxGraphView.prototype.updateStyle = false;
/**
* Variable: lastNode
*
* During validation, this contains the last DOM node that was processed.
*/
mxGraphView.prototype.lastNode = null;
/**
* Variable: lastHtmlNode
*
* During validation, this contains the last HTML DOM node that was processed.
*/
mxGraphView.prototype.lastHtmlNode = null;
/**
* Variable: lastForegroundNode
*
* During validation, this contains the last edge's DOM node that was processed.
*/
mxGraphView.prototype.lastForegroundNode = null;
/**
* Variable: lastForegroundHtmlNode
*
* During validation, this contains the last edge HTML DOM node that was processed.
*/
mxGraphView.prototype.lastForegroundHtmlNode = null;
/**
* Function: getGraphBounds
*
* Returns <graphBounds>.
*/
mxGraphView.prototype.getGraphBounds = function()
{
return this.graphBounds;
};
/**
* Function: setGraphBounds
*
* Sets <graphBounds>.
*/
mxGraphView.prototype.setGraphBounds = function(value)
{
this.graphBounds = value;
};
/**
* Function: getBounds
*
* Returns the union of all <mxCellStates> for the given array of <mxCells>.
*
* Parameters:
*
* cells - Array of <mxCells> whose bounds should be returned.
*/
mxGraphView.prototype.getBounds = function(cells)
{
var result = null;
if (cells != null && cells.length > 0)
{
var model = this.graph.getModel();
for (var i = 0; i < cells.length; i++)
{
if (model.isVertex(cells[i]) || model.isEdge(cells[i]))
{
var state = this.getState(cells[i]);
if (state != null)
{
if (result == null)
{
result = mxRectangle.fromRectangle(state);
}
else
{
result.add(state);
}
}
}
}
}
return result;
};
/**
* Function: setCurrentRoot
*
* Sets and returns the current root and fires an <undo> event before
* calling <mxGraph.sizeDidChange>.
*
* Parameters:
*
* root - <mxCell> that specifies the root of the displayed cell hierarchy.
*/
mxGraphView.prototype.setCurrentRoot = function(root)
{
if (this.currentRoot != root)
{
var change = new mxCurrentRootChange(this, root);
change.execute();
var edit = new mxUndoableEdit(this, true);
edit.add(change);
this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
this.graph.sizeDidChange();
}
return root;
};
/**
* Function: scaleAndTranslate
*
* Sets the scale and translation and fires a <scale> and <translate> event
* before calling <revalidate> followed by <mxGraph.sizeDidChange>.
*
* Parameters:
*
* scale - Decimal value that specifies the new scale (1 is 100%).
* dx - X-coordinate of the translation.
* dy - Y-coordinate of the translation.
*/
mxGraphView.prototype.scaleAndTranslate = function(scale, dx, dy)
{
var previousScale = this.scale;
var previousTranslate = new mxPoint(this.translate.x, this.translate.y);
if (this.scale != scale || this.translate.x != dx || this.translate.y != dy)
{
this.scale = scale;
this.translate.x = dx;
this.translate.y = dy;
if (this.isEventsEnabled())
{
this.viewStateChanged();
}
}
this.fireEvent(new mxEventObject(mxEvent.SCALE_AND_TRANSLATE,
'scale', scale, 'previousScale', previousScale,
'translate', this.translate, 'previousTranslate', previousTranslate));
};
/**
* Function: getScale
*
* Returns the <scale>.
*/
mxGraphView.prototype.getScale = function()
{
return this.scale;
};
/**
* Function: setScale
*
* Sets the scale and fires a <scale> event before calling <revalidate> followed
* by <mxGraph.sizeDidChange>.
*
* Parameters:
*
* value - Decimal value that specifies the new scale (1 is 100%).
*/
mxGraphView.prototype.setScale = function(value)
{
var previousScale = this.scale;
if (this.scale != value)
{
this.scale = value;
if (this.isEventsEnabled())
{
this.viewStateChanged();
}
}
this.fireEvent(new mxEventObject(mxEvent.SCALE,
'scale', value, 'previousScale', previousScale));
};
/**
* Function: getTranslate
*
* Returns the <translate>.
*/
mxGraphView.prototype.getTranslate = function()
{
return this.translate;
};
/**
* Function: setTranslate
*
* Sets the translation and fires a <translate> event before calling
* <revalidate> followed by <mxGraph.sizeDidChange>. The translation is the
* negative of the origin.
*
* Parameters:
*
* dx - X-coordinate of the translation.
* dy - Y-coordinate of the translation.
*/
mxGraphView.prototype.setTranslate = function(dx, dy)
{
var previousTranslate = new mxPoint(this.translate.x, this.translate.y);
if (this.translate.x != dx || this.translate.y != dy)
{
this.translate.x = dx;
this.translate.y = dy;
if (this.isEventsEnabled())
{
this.viewStateChanged();
}
}
this.fireEvent(new mxEventObject(mxEvent.TRANSLATE,
'translate', this.translate, 'previousTranslate', previousTranslate));
};
/**
* Function: viewStateChanged
*
* Invoked after <scale> and/or <translate> has changed.
*/
mxGraphView.prototype.viewStateChanged = function()
{
this.revalidate();
this.graph.sizeDidChange();
};
/**
* Function: refresh
*
* Clears the view if <currentRoot> is not null and revalidates.
*/
mxGraphView.prototype.refresh = function()
{
if (this.currentRoot != null)
{
this.clear();
}
this.revalidate();
};
/**
* Function: revalidate
*
* Revalidates the complete view with all cell states.
*/
mxGraphView.prototype.revalidate = function()
{
this.invalidate();
this.validate();
};
/**
* Function: clear
*
* Removes the state of the given cell and all descendants if the given
* cell is not the current root.
*
* Parameters:
*
* cell - Optional <mxCell> for which the state should be removed. Default
* is the root of the model.
* force - Boolean indicating if the current root should be ignored for
* recursion.
*/
mxGraphView.prototype.clear = function(cell, force, recurse)
{
var model = this.graph.getModel();
cell = cell || model.getRoot();
force = (force != null) ? force : false;
recurse = (recurse != null) ? recurse : true;
this.removeState(cell);
if (recurse && (force || cell != this.currentRoot))
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.clear(model.getChildAt(cell, i), force);
}
}
else
{
this.invalidate(cell);
}
};
/**
* Function: invalidate
*
* Invalidates the state of the given cell, all its descendants and
* connected edges.
*
* Parameters:
*
* cell - Optional <mxCell> to be invalidated. Default is the root of the
* model.
*/
mxGraphView.prototype.invalidate = function(cell, recurse, includeEdges)
{
var model = this.graph.getModel();
cell = cell || model.getRoot();
recurse = (recurse != null) ? recurse : true;
includeEdges = (includeEdges != null) ? includeEdges : true;
var state = this.getState(cell);
if (state != null)
{
state.invalid = true;
}
// Avoids infinite loops for invalid graphs
if (!cell.invalidating)
{
cell.invalidating = true;
// Recursively invalidates all descendants
if (recurse)
{
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
this.invalidate(child, recurse, includeEdges);
}
}
// Propagates invalidation to all connected edges
if (includeEdges)
{
var edgeCount = model.getEdgeCount(cell);
for (var i = 0; i < edgeCount; i++)
{
this.invalidate(model.getEdgeAt(cell, i), recurse, includeEdges);
}
}
delete cell.invalidating;
}
};
/**
* Function: validate
*
* Calls <validateCell> and <validateCellState> and updates the <graphBounds>
* using <getBoundingBox>. Finally the background is validated using
* <validateBackground>.
*
* Parameters:
*
* cell - Optional <mxCell> to be used as the root of the validation.
* Default is <currentRoot> or the root of the model.
*/
mxGraphView.prototype.validate = function(cell)
{
var t0 = mxLog.enter('mxGraphView.validate');
window.status = mxResources.get(this.updatingDocumentResource) ||
this.updatingDocumentResource;
this.resetValidationState();
// Improves IE rendering speed by minimizing reflows
var prevDisplay = null;
if (this.optimizeVmlReflows && this.canvas != null && this.textDiv == null &&
((document.documentMode == 8 && !mxClient.IS_EM) || mxClient.IS_QUIRKS))
{
// Placeholder keeps scrollbar positions when canvas is hidden
this.placeholder = document.createElement('div');
this.placeholder.style.position = 'absolute';
this.placeholder.style.width = this.canvas.clientWidth + 'px';
this.placeholder.style.height = this.canvas.clientHeight + 'px';
this.canvas.parentNode.appendChild(this.placeholder);
prevDisplay = this.drawPane.style.display;
this.canvas.style.display = 'none';
// Creates temporary DIV used for text measuring in mxText.updateBoundingBox
this.textDiv = document.createElement('div');
this.textDiv.style.position = 'absolute';
this.textDiv.style.whiteSpace = 'nowrap';
this.textDiv.style.visibility = 'hidden';
this.textDiv.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
this.textDiv.style.zoom = '1';
document.body.appendChild(this.textDiv);
}
var graphBounds = this.getBoundingBox(this.validateCellState(
this.validateCell(cell || ((this.currentRoot != null) ?
this.currentRoot : this.graph.getModel().getRoot()))));
this.setGraphBounds((graphBounds != null) ? graphBounds : this.getEmptyBounds());
this.validateBackground();
if (prevDisplay != null)
{
this.canvas.style.display = prevDisplay;
this.textDiv.parentNode.removeChild(this.textDiv);
if (this.placeholder != null)
{
this.placeholder.parentNode.removeChild(this.placeholder);
}
// Textdiv cannot be reused
this.textDiv = null;
}
this.resetValidationState();
window.status = mxResources.get(this.doneResource) ||
this.doneResource;
mxLog.leave('mxGraphView.validate', t0);
};
/**
* Function: getEmptyBounds
*
* Returns the bounds for an empty graph. This returns a rectangle at
* <translate> with the size of 0 x 0.
*/
mxGraphView.prototype.getEmptyBounds = function()
{
return new mxRectangle(this.translate.x * this.scale, this.translate.y * this.scale);
};
/**
* Function: getBoundingBox
*
* Returns the bounding box of the shape and the label for the given
* <mxCellState> and its children if recurse is true.
*
* Parameters:
*
* state - <mxCellState> whose bounding box should be returned.
* recurse - Optional boolean indicating if the children should be included.
* Default is true.
*/
mxGraphView.prototype.getBoundingBox = function(state, recurse)
{
recurse = (recurse != null) ? recurse : true;
var bbox = null;
if (state != null)
{
if (state.shape != null && state.shape.boundingBox != null)
{
bbox = state.shape.boundingBox.clone();
}
// Adds label bounding box to graph bounds
if (state.text != null && state.text.boundingBox != null)
{
if (bbox != null)
{
bbox.add(state.text.boundingBox);
}
else
{
bbox = state.text.boundingBox.clone();
}
}
if (recurse)
{
var model = this.graph.getModel();
var childCount = model.getChildCount(state.cell);
for (var i = 0; i < childCount; i++)
{
var bounds = this.getBoundingBox(this.getState(model.getChildAt(state.cell, i)));
if (bounds != null)
{
if (bbox == null)
{
bbox = bounds;
}
else
{
bbox.add(bounds);
}
}
}
}
}
return bbox;
};
/**
* Function: createBackgroundPageShape
*
* Creates and returns the shape used as the background page.
*
* Parameters:
*
* bounds - <mxRectangle> that represents the bounds of the shape.
*/
mxGraphView.prototype.createBackgroundPageShape = function(bounds)
{
return new mxRectangleShape(bounds, 'white', 'black');
};
/**
* Function: validateBackground
*
* Calls <validateBackgroundImage> and <validateBackgroundPage>.
*/
mxGraphView.prototype.validateBackground = function()
{
this.validateBackgroundImage();
this.validateBackgroundPage();
};
/**
* Function: validateBackgroundImage
*
* Validates the background image.
*/
mxGraphView.prototype.validateBackgroundImage = function()
{
var bg = this.graph.getBackgroundImage();
if (bg != null)
{
if (this.backgroundImage == null || this.backgroundImage.image != bg.src)
{
if (this.backgroundImage != null)
{
this.backgroundImage.destroy();
}
var bounds = new mxRectangle(0, 0, 1, 1);
this.backgroundImage = new mxImageShape(bounds, bg.src);
this.backgroundImage.dialect = this.graph.dialect;
this.backgroundImage.init(this.backgroundPane);
this.backgroundImage.redraw();
// Workaround for ignored event on background in IE8 standards mode
if (document.documentMode == 8 && !mxClient.IS_EM)
{
mxEvent.addGestureListeners(this.backgroundImage.node,
mxUtils.bind(this, function(evt)
{
this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
}),
mxUtils.bind(this, function(evt)
{
this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
}),
mxUtils.bind(this, function(evt)
{
this.graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
})
);
}
}
this.redrawBackgroundImage(this.backgroundImage, bg);
}
else if (this.backgroundImage != null)
{
this.backgroundImage.destroy();
this.backgroundImage = null;
}
};
/**
* Function: validateBackgroundPage
*
* Validates the background page.
*/
mxGraphView.prototype.validateBackgroundPage = function()
{
if (this.graph.pageVisible)
{
var bounds = this.getBackgroundPageBounds();
if (this.backgroundPageShape == null)
{
this.backgroundPageShape = this.createBackgroundPageShape(bounds);
this.backgroundPageShape.scale = this.scale;
this.backgroundPageShape.isShadow = true;
this.backgroundPageShape.dialect = this.graph.dialect;
this.backgroundPageShape.init(this.backgroundPane);
this.backgroundPageShape.redraw();
// Adds listener for double click handling on background
if (this.graph.nativeDblClickEnabled)
{
mxEvent.addListener(this.backgroundPageShape.node, 'dblclick', mxUtils.bind(this, function(evt)
{
this.graph.dblClick(evt);
}));
}
// Adds basic listeners for graph event dispatching outside of the
// container and finishing the handling of a single gesture
mxEvent.addGestureListeners(this.backgroundPageShape.node,
mxUtils.bind(this, function(evt)
{
this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
}),
mxUtils.bind(this, function(evt)
{
// Hides the tooltip if mouse is outside container
if (this.graph.tooltipHandler != null && this.graph.tooltipHandler.isHideOnHover())
{
this.graph.tooltipHandler.hide();
}
if (this.graph.isMouseDown && !mxEvent.isConsumed(evt))
{
this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
}
}),
mxUtils.bind(this, function(evt)
{
this.graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
})
);
}
else
{
this.backgroundPageShape.scale = this.scale;
this.backgroundPageShape.bounds = bounds;
this.backgroundPageShape.redraw();
}
}
else if (this.backgroundPageShape != null)
{
this.backgroundPageShape.destroy();
this.backgroundPageShape = null;
}
};
/**
* Function: getBackgroundPageBounds
*
* Returns the bounds for the background page.
*/
mxGraphView.prototype.getBackgroundPageBounds = function()
{
var fmt = this.graph.pageFormat;
var ps = this.scale * this.graph.pageScale;
var bounds = new mxRectangle(this.scale * this.translate.x, this.scale * this.translate.y,
fmt.width * ps, fmt.height * ps);
return bounds;
};
/**
* Function: redrawBackgroundImage
*
* Updates the bounds and redraws the background image.
*
* Example:
*
* If the background image should not be scaled, this can be replaced with
* the following.
*
* (code)
* mxGraphView.prototype.redrawBackground = function(backgroundImage, bg)
* {
* backgroundImage.bounds.x = this.translate.x;
* backgroundImage.bounds.y = this.translate.y;
* backgroundImage.bounds.width = bg.width;
* backgroundImage.bounds.height = bg.height;
*
* backgroundImage.redraw();
* };
* (end)
*
* Parameters:
*
* backgroundImage - <mxImageShape> that represents the background image.
* bg - <mxImage> that specifies the image and its dimensions.
*/
mxGraphView.prototype.redrawBackgroundImage = function(backgroundImage, bg)
{
backgroundImage.scale = this.scale;
backgroundImage.bounds.x = this.scale * this.translate.x;
backgroundImage.bounds.y = this.scale * this.translate.y;
backgroundImage.bounds.width = this.scale * bg.width;
backgroundImage.bounds.height = this.scale * bg.height;
backgroundImage.redraw();
};
/**
* Function: validateCell
*
* Recursively creates the cell state for the given cell if visible is true and
* the given cell is visible. If the cell is not visible but the state exists
* then it is removed using <removeState>.
*
* Parameters:
*
* cell - <mxCell> whose <mxCellState> should be created.
* visible - Optional boolean indicating if the cell should be visible. Default
* is true.
*/
mxGraphView.prototype.validateCell = function(cell, visible)
{
visible = (visible != null) ? visible : true;
if (cell != null)
{
visible = visible && this.graph.isCellVisible(cell);
var state = this.getState(cell, visible);
if (state != null && !visible)
{
this.removeState(cell);
}
else
{
var model = this.graph.getModel();
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.validateCell(model.getChildAt(cell, i), visible &&
(!this.isCellCollapsed(cell) || cell == this.currentRoot));
}
}
}
return cell;
};
/**
* Function: validateCellState
*
* Validates and repaints the <mxCellState> for the given <mxCell>.
*
* Parameters:
*
* cell - <mxCell> whose <mxCellState> should be validated.
* recurse - Optional boolean indicating if the children of the cell should be
* validated. Default is true.
*/
mxGraphView.prototype.validateCellState = function(cell, recurse)
{
recurse = (recurse != null) ? recurse : true;
var state = null;
if (cell != null)
{
state = this.getState(cell);
if (state != null)
{
var model = this.graph.getModel();
if (state.invalid)
{
state.invalid = false;
if (state.style == null || state.invalidStyle)
{
state.style = this.graph.getCellStyle(state.cell);
state.invalidStyle = false;
}
if (cell != this.currentRoot)
{
this.validateCellState(model.getParent(cell), false);
}
state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, true), false), true);
state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, false), false), false);
this.updateCellState(state);
// Repaint happens immediately after the cell is validated
if (cell != this.currentRoot && !state.invalid)
{
this.graph.cellRenderer.redraw(state, false, this.isRendering());
// Handles changes to invertex paintbounds after update of rendering shape
state.updateCachedBounds();
}
}
if (recurse && !state.invalid)
{
// Updates order in DOM if recursively traversing
if (state.shape != null)
{
this.stateValidated(state);
}
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.validateCellState(model.getChildAt(cell, i));
}
}
}
}
return state;
};
/**
* Function: updateCellState
*
* Updates the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to be updated.
*/
mxGraphView.prototype.updateCellState = function(state)
{
state.absoluteOffset.x = 0;
state.absoluteOffset.y = 0;
state.origin.x = 0;
state.origin.y = 0;
state.length = 0;
if (state.cell != this.currentRoot)
{
var model = this.graph.getModel();
var pState = this.getState(model.getParent(state.cell));
if (pState != null && pState.cell != this.currentRoot)
{
state.origin.x += pState.origin.x;
state.origin.y += pState.origin.y;
}
var offset = this.graph.getChildOffsetForCell(state.cell);
if (offset != null)
{
state.origin.x += offset.x;
state.origin.y += offset.y;
}
var geo = this.graph.getCellGeometry(state.cell);
if (geo != null)
{
if (!model.isEdge(state.cell))
{
offset = geo.offset || this.EMPTY_POINT;
if (geo.relative && pState != null)
{
if (model.isEdge(pState.cell))
{
var origin = this.getPoint(pState, geo);
if (origin != null)
{
state.origin.x += (origin.x / this.scale) - pState.origin.x - this.translate.x;
state.origin.y += (origin.y / this.scale) - pState.origin.y - this.translate.y;
}
}
else
{
state.origin.x += geo.x * pState.width / this.scale + offset.x;
state.origin.y += geo.y * pState.height / this.scale + offset.y;
}
}
else
{
state.absoluteOffset.x = this.scale * offset.x;
state.absoluteOffset.y = this.scale * offset.y;
state.origin.x += geo.x;
state.origin.y += geo.y;
}
}
state.x = this.scale * (this.translate.x + state.origin.x);
state.y = this.scale * (this.translate.y + state.origin.y);
state.width = this.scale * geo.width;
state.unscaledWidth = geo.width;
state.height = this.scale * geo.height;
if (model.isVertex(state.cell))
{
this.updateVertexState(state, geo);
}
if (model.isEdge(state.cell))
{
this.updateEdgeState(state, geo);
}
}
}
state.updateCachedBounds();
};
/**
* Function: isCellCollapsed
*
* Returns true if the children of the given cell should not be visible in the
* view. This implementation uses <mxGraph.isCellVisible> but it can be
* overidden to use a separate condition.
*/
mxGraphView.prototype.isCellCollapsed = function(cell)
{
return this.graph.isCellCollapsed(cell);
};
/**
* Function: updateVertexState
*
* Validates the given cell state.
*/
mxGraphView.prototype.updateVertexState = function(state, geo)
{
var model = this.graph.getModel();
var pState = this.getState(model.getParent(state.cell));
if (geo.relative && pState != null && !model.isEdge(pState.cell))
{
var alpha = mxUtils.toRadians(pState.style[mxConstants.STYLE_ROTATION] || '0');
if (alpha != 0)
{
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var ct = new mxPoint(state.getCenterX(), state.getCenterY());
var cx = new mxPoint(pState.getCenterX(), pState.getCenterY());
var pt = mxUtils.getRotatedPoint(ct, cos, sin, cx);
state.x = pt.x - state.width / 2;
state.y = pt.y - state.height / 2;
}
}
this.updateVertexLabelOffset(state);
};
/**
* Function: updateEdgeState
*
* Validates the given cell state.
*/
mxGraphView.prototype.updateEdgeState = function(state, geo)
{
var source = state.getVisibleTerminalState(true);
var target = state.getVisibleTerminalState(false);
// This will remove edges with no terminals and no terminal points
// as such edges are invalid and produce NPEs in the edge styles.
// Also removes connected edges that have no visible terminals.
if ((this.graph.model.getTerminal(state.cell, true) != null && source == null) ||
(source == null && geo.getTerminalPoint(true) == null) ||
(this.graph.model.getTerminal(state.cell, false) != null && target == null) ||
(target == null && geo.getTerminalPoint(false) == null))
{
this.clear(state.cell, true);
}
else
{
this.updateFixedTerminalPoints(state, source, target);
this.updatePoints(state, geo.points, source, target);
this.updateFloatingTerminalPoints(state, source, target);
var pts = state.absolutePoints;
if (state.cell != this.currentRoot && (pts == null || pts.length < 2 ||
pts[0] == null || pts[pts.length - 1] == null))
{
// This will remove edges with invalid points from the list of states in the view.
// Happens if the one of the terminals and the corresponding terminal point is null.
this.clear(state.cell, true);
}
else
{
this.updateEdgeBounds(state);
this.updateEdgeLabelOffset(state);
}
}
};
/**
* Function: updateVertexLabelOffset
*
* Updates the absoluteOffset of the given vertex cell state. This takes
* into account the label position styles.
*
* Parameters:
*
* state - <mxCellState> whose absolute offset should be updated.
*/
mxGraphView.prototype.updateVertexLabelOffset = function(state)
{
var h = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
if (h == mxConstants.ALIGN_LEFT)
{
var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
if (lw != null)
{
lw *= this.scale;
}
else
{
lw = state.width;
}
state.absoluteOffset.x -= lw;
}
else if (h == mxConstants.ALIGN_RIGHT)
{
state.absoluteOffset.x += state.width;
}
else if (h == mxConstants.ALIGN_CENTER)
{
var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
if (lw != null)
{
// Aligns text block with given width inside the vertex width
var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER);
var dx = 0;
if (align == mxConstants.ALIGN_CENTER)
{
dx = 0.5;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
dx = 1;
}
if (dx != 0)
{
state.absoluteOffset.x -= (lw * this.scale - state.width) * dx;
}
}
}
var v = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
if (v == mxConstants.ALIGN_TOP)
{
state.absoluteOffset.y -= state.height;
}
else if (v == mxConstants.ALIGN_BOTTOM)
{
state.absoluteOffset.y += state.height;
}
};
/**
* Function: resetValidationState
*
* Resets the current validation state.
*/
mxGraphView.prototype.resetValidationState = function()
{
this.lastNode = null;
this.lastHtmlNode = null;
this.lastForegroundNode = null;
this.lastForegroundHtmlNode = null;
};
/**
* Function: stateValidated
*
* Invoked when a state has been processed in <validatePoints>. This is used
* to update the order of the DOM nodes of the shape.
*
* Parameters:
*
* state - <mxCellState> that represents the cell state.
*/
mxGraphView.prototype.stateValidated = function(state)
{
var fg = (this.graph.getModel().isEdge(state.cell) && this.graph.keepEdgesInForeground) ||
(this.graph.getModel().isVertex(state.cell) && this.graph.keepEdgesInBackground);
var htmlNode = (fg) ? this.lastForegroundHtmlNode || this.lastHtmlNode : this.lastHtmlNode;
var node = (fg) ? this.lastForegroundNode || this.lastNode : this.lastNode;
var result = this.graph.cellRenderer.insertStateAfter(state, node, htmlNode);
if (fg)
{
this.lastForegroundHtmlNode = result[1];
this.lastForegroundNode = result[0];
}
else
{
this.lastHtmlNode = result[1];
this.lastNode = result[0];
}
};
/**
* Function: updateFixedTerminalPoints
*
* Sets the initial absolute terminal points in the given state before the edge
* style is computed.
*
* Parameters:
*
* edge - <mxCellState> whose initial terminal points should be updated.
* source - <mxCellState> which represents the source terminal.
* target - <mxCellState> which represents the target terminal.
*/
mxGraphView.prototype.updateFixedTerminalPoints = function(edge, source, target)
{
this.updateFixedTerminalPoint(edge, source, true,
this.graph.getConnectionConstraint(edge, source, true));
this.updateFixedTerminalPoint(edge, target, false,
this.graph.getConnectionConstraint(edge, target, false));
};
/**
* Function: updateFixedTerminalPoint
*
* Sets the fixed source or target terminal point on the given edge.
*
* Parameters:
*
* edge - <mxCellState> whose terminal point should be updated.
* terminal - <mxCellState> which represents the actual terminal.
* source - Boolean that specifies if the terminal is the source.
* constraint - <mxConnectionConstraint> that specifies the connection.
*/
mxGraphView.prototype.updateFixedTerminalPoint = function(edge, terminal, source, constraint)
{
edge.setAbsoluteTerminalPoint(this.getFixedTerminalPoint(edge, terminal, source, constraint), source);
};
/**
* Function: getFixedTerminalPoint
*
* Returns the fixed source or target terminal point for the given edge.
*
* Parameters:
*
* edge - <mxCellState> whose terminal point should be returned.
* terminal - <mxCellState> which represents the actual terminal.
* source - Boolean that specifies if the terminal is the source.
* constraint - <mxConnectionConstraint> that specifies the connection.
*/
mxGraphView.prototype.getFixedTerminalPoint = function(edge, terminal, source, constraint)
{
var pt = null;
if (constraint != null)
{
pt = this.graph.getConnectionPoint(terminal, constraint, this.graph.isOrthogonal(edge));
}
if (pt == null && terminal == null)
{
var s = this.scale;
var tr = this.translate;
var orig = edge.origin;
var geo = this.graph.getCellGeometry(edge.cell);
pt = geo.getTerminalPoint(source);
if (pt != null)
{
pt = new mxPoint(s * (tr.x + pt.x + orig.x),
s * (tr.y + pt.y + orig.y));
}
}
return pt;
};
/**
* Function: updateBoundsFromStencil
*
* Updates the bounds of the given cell state to reflect the bounds of the stencil
* if it has a fixed aspect and returns the previous bounds as an <mxRectangle> if
* the bounds have been modified or null otherwise.
*
* Parameters:
*
* edge - <mxCellState> whose bounds should be updated.
*/
mxGraphView.prototype.updateBoundsFromStencil = function(state)
{
var previous = null;
if (state != null && state.shape != null && state.shape.stencil != null && state.shape.stencil.aspect == 'fixed')
{
previous = mxRectangle.fromRectangle(state);
var asp = state.shape.stencil.computeAspect(state.style, state.x, state.y, state.width, state.height);
state.setRect(asp.x, asp.y, state.shape.stencil.w0 * asp.width, state.shape.stencil.h0 * asp.height);
}
return previous;
};
/**
* Function: updatePoints
*
* Updates the absolute points in the given state using the specified array
* of <mxPoints> as the relative points.
*
* Parameters:
*
* edge - <mxCellState> whose absolute points should be updated.
* points - Array of <mxPoints> that constitute the relative points.
* source - <mxCellState> that represents the source terminal.
* target - <mxCellState> that represents the target terminal.
*/
mxGraphView.prototype.updatePoints = function(edge, points, source, target)
{
if (edge != null)
{
var pts = [];
pts.push(edge.absolutePoints[0]);
var edgeStyle = this.getEdgeStyle(edge, points, source, target);
if (edgeStyle != null)
{
var src = this.getTerminalPort(edge, source, true);
var trg = this.getTerminalPort(edge, target, false);
// Uses the stencil bounds for routing and restores after routing
var srcBounds = this.updateBoundsFromStencil(src);
var trgBounds = this.updateBoundsFromStencil(trg);
edgeStyle(edge, src, trg, points, pts);
// Restores previous bounds
if (srcBounds != null)
{
src.setRect(srcBounds.x, srcBounds.y, srcBounds.width, srcBounds.height);
}
if (trgBounds != null)
{
trg.setRect(trgBounds.x, trgBounds.y, trgBounds.width, trgBounds.height);
}
}
else if (points != null)
{
for (var i = 0; i < points.length; i++)
{
if (points[i] != null)
{
var pt = mxUtils.clone(points[i]);
pts.push(this.transformControlPoint(edge, pt));
}
}
}
var tmp = edge.absolutePoints;
pts.push(tmp[tmp.length-1]);
edge.absolutePoints = pts;
}
};
/**
* Function: transformControlPoint
*
* Transforms the given control point to an absolute point.
*/
mxGraphView.prototype.transformControlPoint = function(state, pt)
{
if (state != null && pt != null)
{
var orig = state.origin;
return new mxPoint(this.scale * (pt.x + this.translate.x + orig.x),
this.scale * (pt.y + this.translate.y + orig.y));
}
return null;
};
/**
* Function: isLoopStyleEnabled
*
* Returns true if the given edge should be routed with <mxGraph.defaultLoopStyle>
* or the <mxConstants.STYLE_LOOP> defined for the given edge. This implementation
* returns true if the given edge is a loop and does not have connections constraints
* associated.
*/
mxGraphView.prototype.isLoopStyleEnabled = function(edge, points, source, target)
{
var sc = this.graph.getConnectionConstraint(edge, source, true);
var tc = this.graph.getConnectionConstraint(edge, target, false);
if ((points == null || points.length < 2) &&
(!mxUtils.getValue(edge.style, mxConstants.STYLE_ORTHOGONAL_LOOP, false) ||
((sc == null || sc.point == null) && (tc == null || tc.point == null))))
{
return source != null && source == target;
}
return false;
};
/**
* Function: getEdgeStyle
*
* Returns the edge style function to be used to render the given edge state.
*/
mxGraphView.prototype.getEdgeStyle = function(edge, points, source, target)
{
var edgeStyle = this.isLoopStyleEnabled(edge, points, source, target) ?
mxUtils.getValue(edge.style, mxConstants.STYLE_LOOP, this.graph.defaultLoopStyle) :
(!mxUtils.getValue(edge.style, mxConstants.STYLE_NOEDGESTYLE, false) ?
edge.style[mxConstants.STYLE_EDGE] : null);
// Converts string values to objects
if (typeof(edgeStyle) == "string")
{
var tmp = mxStyleRegistry.getValue(edgeStyle);
if (tmp == null && this.isAllowEval())
{
tmp = mxUtils.eval(edgeStyle);
}
edgeStyle = tmp;
}
if (typeof(edgeStyle) == "function")
{
return edgeStyle;
}
return null;
};
/**
* Function: updateFloatingTerminalPoints
*
* Updates the terminal points in the given state after the edge style was
* computed for the edge.
*
* Parameters:
*
* state - <mxCellState> whose terminal points should be updated.
* source - <mxCellState> that represents the source terminal.
* target - <mxCellState> that represents the target terminal.
*/
mxGraphView.prototype.updateFloatingTerminalPoints = function(state, source, target)
{
var pts = state.absolutePoints;
var p0 = pts[0];
var pe = pts[pts.length - 1];
if (pe == null && target != null)
{
this.updateFloatingTerminalPoint(state, target, source, false);
}
if (p0 == null && source != null)
{
this.updateFloatingTerminalPoint(state, source, target, true);
}
};
/**
* Function: updateFloatingTerminalPoint
*
* Updates the absolute terminal point in the given state for the given
* start and end state, where start is the source if source is true.
*
* Parameters:
*
* edge - <mxCellState> whose terminal point should be updated.
* start - <mxCellState> for the terminal on "this" side of the edge.
* end - <mxCellState> for the terminal on the other side of the edge.
* source - Boolean indicating if start is the source terminal state.
*/
mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source)
{
edge.setAbsoluteTerminalPoint(this.getFloatingTerminalPoint(edge, start, end, source), source);
};
/**
* Function: getFloatingTerminalPoint
*
* Returns the floating terminal point for the given edge, start and end
* state, where start is the source if source is true.
*
* Parameters:
*
* edge - <mxCellState> whose terminal point should be returned.
* start - <mxCellState> for the terminal on "this" side of the edge.
* end - <mxCellState> for the terminal on the other side of the edge.
* source - Boolean indicating if start is the source terminal state.
*/
mxGraphView.prototype.getFloatingTerminalPoint = function(edge, start, end, source)
{
start = this.getTerminalPort(edge, start, source);
var next = this.getNextPoint(edge, end, source);
var orth = this.graph.isOrthogonal(edge);
var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0'));
var center = new mxPoint(start.getCenterX(), start.getCenterY());
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
next = mxUtils.getRotatedPoint(next, cos, sin, center);
}
var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0);
border += parseFloat(edge.style[(source) ?
mxConstants.STYLE_SOURCE_PERIMETER_SPACING :
mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0);
var pt = this.getPerimeterPoint(start, next, alpha == 0 && orth, border);
if (alpha != 0)
{
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
pt = mxUtils.getRotatedPoint(pt, cos, sin, center);
}
return pt;
};
/**
* Function: getTerminalPort
*
* Returns an <mxCellState> that represents the source or target terminal or
* port for the given edge.
*
* Parameters:
*
* state - <mxCellState> that represents the state of the edge.
* terminal - <mxCellState> that represents the terminal.
* source - Boolean indicating if the given terminal is the source terminal.
*/
mxGraphView.prototype.getTerminalPort = function(state, terminal, source)
{
var key = (source) ? mxConstants.STYLE_SOURCE_PORT :
mxConstants.STYLE_TARGET_PORT;
var id = mxUtils.getValue(state.style, key);
if (id != null)
{
var tmp = this.getState(this.graph.getModel().getCell(id));
// Only uses ports where a cell state exists
if (tmp != null)
{
terminal = tmp;
}
}
return terminal;
};
/**
* Function: getPerimeterPoint
*
* Returns an <mxPoint> that defines the location of the intersection point between
* the perimeter and the line between the center of the shape and the given point.
*
* Parameters:
*
* terminal - <mxCellState> for the source or target terminal.
* next - <mxPoint> that lies outside of the given terminal.
* orthogonal - Boolean that specifies if the orthogonal projection onto
* the perimeter should be returned. If this is false then the intersection
* of the perimeter and the line between the next and the center point is
* returned.
* border - Optional border between the perimeter and the shape.
*/
mxGraphView.prototype.getPerimeterPoint = function(terminal, next, orthogonal, border)
{
var point = null;
if (terminal != null)
{
var perimeter = this.getPerimeterFunction(terminal);
if (perimeter != null && next != null)
{
var bounds = this.getPerimeterBounds(terminal, border);
if (bounds.width > 0 || bounds.height > 0)
{
point = new mxPoint(next.x, next.y);
var flipH = false;
var flipV = false;
if (this.graph.model.isVertex(terminal.cell))
{
flipH = mxUtils.getValue(terminal.style, mxConstants.STYLE_FLIPH, 0) == 1;
flipV = mxUtils.getValue(terminal.style, mxConstants.STYLE_FLIPV, 0) == 1;
// Legacy support for stencilFlipH/V
if (terminal.shape != null && terminal.shape.stencil != null)
{
flipH = (mxUtils.getValue(terminal.style, 'stencilFlipH', 0) == 1) || flipH;
flipV = (mxUtils.getValue(terminal.style, 'stencilFlipV', 0) == 1) || flipV;
}
if (flipH)
{
point.x = 2 * bounds.getCenterX() - point.x;
}
if (flipV)
{
point.y = 2 * bounds.getCenterY() - point.y;
}
}
point = perimeter(bounds, terminal, point, orthogonal);
if (point != null)
{
if (flipH)
{
point.x = 2 * bounds.getCenterX() - point.x;
}
if (flipV)
{
point.y = 2 * bounds.getCenterY() - point.y;
}
}
}
}
if (point == null)
{
point = this.getPoint(terminal);
}
}
return point;
};
/**
* Function: getRoutingCenterX
*
* Returns the x-coordinate of the center point for automatic routing.
*/
mxGraphView.prototype.getRoutingCenterX = function (state)
{
var f = (state.style != null) ? parseFloat(state.style
[mxConstants.STYLE_ROUTING_CENTER_X]) || 0 : 0;
return state.getCenterX() + f * state.width;
};
/**
* Function: getRoutingCenterY
*
* Returns the y-coordinate of the center point for automatic routing.
*/
mxGraphView.prototype.getRoutingCenterY = function (state)
{
var f = (state.style != null) ? parseFloat(state.style
[mxConstants.STYLE_ROUTING_CENTER_Y]) || 0 : 0;
return state.getCenterY() + f * state.height;
};
/**
* Function: getPerimeterBounds
*
* Returns the perimeter bounds for the given terminal, edge pair as an
* <mxRectangle>.
*
* If you have a model where each terminal has a relative child that should
* act as the graphical endpoint for a connection from/to the terminal, then
* this method can be replaced as follows:
*
* (code)
* var oldGetPerimeterBounds = mxGraphView.prototype.getPerimeterBounds;
* mxGraphView.prototype.getPerimeterBounds = function(terminal, edge, isSource)
* {
* var model = this.graph.getModel();
* var childCount = model.getChildCount(terminal.cell);
*
* if (childCount > 0)
* {
* var child = model.getChildAt(terminal.cell, 0);
* var geo = model.getGeometry(child);
*
* if (geo != null &&
* geo.relative)
* {
* var state = this.getState(child);
*
* if (state != null)
* {
* terminal = state;
* }
* }
* }
*
* return oldGetPerimeterBounds.apply(this, arguments);
* };
* (end)
*
* Parameters:
*
* terminal - <mxCellState> that represents the terminal.
* border - Number that adds a border between the shape and the perimeter.
*/
mxGraphView.prototype.getPerimeterBounds = function(terminal, border)
{
border = (border != null) ? border : 0;
if (terminal != null)
{
border += parseFloat(terminal.style[mxConstants.STYLE_PERIMETER_SPACING] || 0);
}
return terminal.getPerimeterBounds(border * this.scale);
};
/**
* Function: getPerimeterFunction
*
* Returns the perimeter function for the given state.
*/
mxGraphView.prototype.getPerimeterFunction = function(state)
{
var perimeter = state.style[mxConstants.STYLE_PERIMETER];
// Converts string values to objects
if (typeof(perimeter) == "string")
{
var tmp = mxStyleRegistry.getValue(perimeter);
if (tmp == null && this.isAllowEval())
{
tmp = mxUtils.eval(perimeter);
}
perimeter = tmp;
}
if (typeof(perimeter) == "function")
{
return perimeter;
}
return null;
};
/**
* Function: getNextPoint
*
* Returns the nearest point in the list of absolute points or the center
* of the opposite terminal.
*
* Parameters:
*
* edge - <mxCellState> that represents the edge.
* opposite - <mxCellState> that represents the opposite terminal.
* source - Boolean indicating if the next point for the source or target
* should be returned.
*/
mxGraphView.prototype.getNextPoint = function(edge, opposite, source)
{
var pts = edge.absolutePoints;
var point = null;
if (pts != null && pts.length >= 2)
{
var count = pts.length;
point = pts[(source) ? Math.min(1, count - 1) : Math.max(0, count - 2)];
}
if (point == null && opposite != null)
{
point = new mxPoint(opposite.getCenterX(), opposite.getCenterY());
}
return point;
};
/**
* Function: getVisibleTerminal
*
* Returns the nearest ancestor terminal that is visible. The edge appears
* to be connected to this terminal on the display. The result of this method
* is cached in <mxCellState.getVisibleTerminalState>.
*
* Parameters:
*
* edge - <mxCell> whose visible terminal should be returned.
* source - Boolean that specifies if the source or target terminal
* should be returned.
*/
mxGraphView.prototype.getVisibleTerminal = function(edge, source)
{
var model = this.graph.getModel();
var result = model.getTerminal(edge, source);
var best = result;
while (result != null && result != this.currentRoot)
{
if (!this.graph.isCellVisible(best) || this.isCellCollapsed(result))
{
best = result;
}
result = model.getParent(result);
}
// Checks if the result is valid for the current view state
if (best != null && (!model.contains(best) ||
model.getParent(best) == model.getRoot() ||
best == this.currentRoot))
{
best = null;
}
return best;
};
/**
* Function: updateEdgeBounds
*
* Updates the given state using the bounding box of t
* he absolute points.
* Also updates <mxCellState.terminalDistance>, <mxCellState.length> and
* <mxCellState.segments>.
*
* Parameters:
*
* state - <mxCellState> whose bounds should be updated.
*/
mxGraphView.prototype.updateEdgeBounds = function(state)
{
var points = state.absolutePoints;
var p0 = points[0];
var pe = points[points.length - 1];
if (p0.x != pe.x || p0.y != pe.y)
{
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
state.terminalDistance = Math.sqrt(dx * dx + dy * dy);
}
else
{
state.terminalDistance = 0;
}
var length = 0;
var segments = [];
var pt = p0;
if (pt != null)
{
var minX = pt.x;
var minY = pt.y;
var maxX = minX;
var maxY = minY;
for (var i = 1; i < points.length; i++)
{
var tmp = points[i];
if (tmp != null)
{
var dx = pt.x - tmp.x;
var dy = pt.y - tmp.y;
var segment = Math.sqrt(dx * dx + dy * dy);
segments.push(segment);
length += segment;
pt = tmp;
minX = Math.min(pt.x, minX);
minY = Math.min(pt.y, minY);
maxX = Math.max(pt.x, maxX);
maxY = Math.max(pt.y, maxY);
}
}
state.length = length;
state.segments = segments;
var markerSize = 1; // TODO: include marker size
state.x = minX;
state.y = minY;
state.width = Math.max(markerSize, maxX - minX);
state.height = Math.max(markerSize, maxY - minY);
}
};
/**
* Function: getPoint
*
* Returns the absolute point on the edge for the given relative
* <mxGeometry> as an <mxPoint>. The edge is represented by the given
* <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> that represents the state of the parent edge.
* geometry - <mxGeometry> that represents the relative location.
*/
mxGraphView.prototype.getPoint = function(state, geometry)
{
var x = state.getCenterX();
var y = state.getCenterY();
if (state.segments != null && (geometry == null || geometry.relative))
{
var gx = (geometry != null) ? geometry.x / 2 : 0;
var pointCount = state.absolutePoints.length;
var dist = Math.round((gx + 0.5) * state.length);
var segment = state.segments[0];
var length = 0;
var index = 1;
while (dist >= Math.round(length + segment) && index < pointCount - 1)
{
length += segment;
segment = state.segments[index++];
}
var factor = (segment == 0) ? 0 : (dist - length) / segment;
var p0 = state.absolutePoints[index-1];
var pe = state.absolutePoints[index];
if (p0 != null && pe != null)
{
var gy = 0;
var offsetX = 0;
var offsetY = 0;
if (geometry != null)
{
gy = geometry.y;
var offset = geometry.offset;
if (offset != null)
{
offsetX = offset.x;
offsetY = offset.y;
}
}
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var nx = (segment == 0) ? 0 : dy / segment;
var ny = (segment == 0) ? 0 : dx / segment;
x = p0.x + dx * factor + (nx * gy + offsetX) * this.scale;
y = p0.y + dy * factor - (ny * gy - offsetY) * this.scale;
}
}
else if (geometry != null)
{
var offset = geometry.offset;
if (offset != null)
{
x += offset.x;
y += offset.y;
}
}
return new mxPoint(x, y);
};
/**
* Function: getRelativePoint
*
* Gets the relative point that describes the given, absolute label
* position for the given edge state.
*
* Parameters:
*
* state - <mxCellState> that represents the state of the parent edge.
* x - Specifies the x-coordinate of the absolute label location.
* y - Specifies the y-coordinate of the absolute label location.
*/
mxGraphView.prototype.getRelativePoint = function(edgeState, x, y)
{
var model = this.graph.getModel();
var geometry = model.getGeometry(edgeState.cell);
if (geometry != null)
{
var pointCount = edgeState.absolutePoints.length;
if (geometry.relative && pointCount > 1)
{
var totalLength = edgeState.length;
var segments = edgeState.segments;
// Works which line segment the point of the label is closest to
var p0 = edgeState.absolutePoints[0];
var pe = edgeState.absolutePoints[1];
var minDist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y);
var index = 0;
var tmp = 0;
var length = 0;
for (var i = 2; i < pointCount; i++)
{
tmp += segments[i - 2];
pe = edgeState.absolutePoints[i];
var dist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y);
if (dist <= minDist)
{
minDist = dist;
index = i - 1;
length = tmp;
}
p0 = pe;
}
var seg = segments[index];
p0 = edgeState.absolutePoints[index];
pe = edgeState.absolutePoints[index + 1];
var x2 = p0.x;
var y2 = p0.y;
var x1 = pe.x;
var y1 = pe.y;
var px = x;
var py = y;
var xSegment = x2 - x1;
var ySegment = y2 - y1;
px -= x1;
py -= y1;
var projlenSq = 0;
px = xSegment - px;
py = ySegment - py;
var dotprod = px * xSegment + py * ySegment;
if (dotprod <= 0.0)
{
projlenSq = 0;
}
else
{
projlenSq = dotprod * dotprod
/ (xSegment * xSegment + ySegment * ySegment);
}
var projlen = Math.sqrt(projlenSq);
if (projlen > seg)
{
projlen = seg;
}
var yDistance = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, pe
.x, pe.y, x, y));
var direction = mxUtils.relativeCcw(p0.x, p0.y, pe.x, pe.y, x, y);
if (direction == -1)
{
yDistance = -yDistance;
}
// Constructs the relative point for the label
return new mxPoint(((totalLength / 2 - length - projlen) / totalLength) * -2,
yDistance / this.scale);
}
}
return new mxPoint();
};
/**
* Function: updateEdgeLabelOffset
*
* Updates <mxCellState.absoluteOffset> for the given state. The absolute
* offset is normally used for the position of the edge label. Is is
* calculated from the geometry as an absolute offset from the center
* between the two endpoints if the geometry is absolute, or as the
* relative distance between the center along the line and the absolute
* orthogonal distance if the geometry is relative.
*
* Parameters:
*
* state - <mxCellState> whose absolute offset should be updated.
*/
mxGraphView.prototype.updateEdgeLabelOffset = function(state)
{
var points = state.absolutePoints;
state.absoluteOffset.x = state.getCenterX();
state.absoluteOffset.y = state.getCenterY();
if (points != null && points.length > 0 && state.segments != null)
{
var geometry = this.graph.getCellGeometry(state.cell);
if (geometry.relative)
{
var offset = this.getPoint(state, geometry);
if (offset != null)
{
state.absoluteOffset = offset;
}
}
else
{
var p0 = points[0];
var pe = points[points.length - 1];
if (p0 != null && pe != null)
{
var dx = pe.x - p0.x;
var dy = pe.y - p0.y;
var x0 = 0;
var y0 = 0;
var off = geometry.offset;
if (off != null)
{
x0 = off.x;
y0 = off.y;
}
var x = p0.x + dx / 2 + x0 * this.scale;
var y = p0.y + dy / 2 + y0 * this.scale;
state.absoluteOffset.x = x;
state.absoluteOffset.y = y;
}
}
}
};
/**
* Function: getState
*
* Returns the <mxCellState> for the given cell. If create is true, then
* the state is created if it does not yet exist.
*
* Parameters:
*
* cell - <mxCell> for which the <mxCellState> should be returned.
* create - Optional boolean indicating if a new state should be created
* if it does not yet exist. Default is false.
*/
mxGraphView.prototype.getState = function(cell, create)
{
create = create || false;
var state = null;
if (cell != null)
{
state = this.states.get(cell);
if (create && (state == null || this.updateStyle) && this.graph.isCellVisible(cell))
{
if (state == null)
{
state = this.createState(cell);
this.states.put(cell, state);
}
else
{
state.style = this.graph.getCellStyle(cell);
}
}
}
return state;
};
/**
* Function: isRendering
*
* Returns <rendering>.
*/
mxGraphView.prototype.isRendering = function()
{
return this.rendering;
};
/**
* Function: setRendering
*
* Sets <rendering>.
*/
mxGraphView.prototype.setRendering = function(value)
{
this.rendering = value;
};
/**
* Function: isAllowEval
*
* Returns <allowEval>.
*/
mxGraphView.prototype.isAllowEval = function()
{
return this.allowEval;
};
/**
* Function: setAllowEval
*
* Sets <allowEval>.
*/
mxGraphView.prototype.setAllowEval = function(value)
{
this.allowEval = value;
};
/**
* Function: getStates
*
* Returns <states>.
*/
mxGraphView.prototype.getStates = function()
{
return this.states;
};
/**
* Function: setStates
*
* Sets <states>.
*/
mxGraphView.prototype.setStates = function(value)
{
this.states = value;
};
/**
* Function: getCellStates
*
* Returns the <mxCellStates> for the given array of <mxCells>. The array
* contains all states that are not null, that is, the returned array may
* have less elements than the given array. If no argument is given, then
* this returns <states>.
*/
mxGraphView.prototype.getCellStates = function(cells)
{
if (cells == null)
{
return this.states;
}
else
{
var result = [];
for (var i = 0; i < cells.length; i++)
{
var state = this.getState(cells[i]);
if (state != null)
{
result.push(state);
}
}
return result;
}
};
/**
* Function: removeState
*
* Removes and returns the <mxCellState> for the given cell.
*
* Parameters:
*
* cell - <mxCell> for which the <mxCellState> should be removed.
*/
mxGraphView.prototype.removeState = function(cell)
{
var state = null;
if (cell != null)
{
state = this.states.remove(cell);
if (state != null)
{
this.graph.cellRenderer.destroy(state);
state.invalid = true;
state.destroy();
}
}
return state;
};
/**
* Function: createState
*
* Creates and returns an <mxCellState> for the given cell and initializes
* it using <mxCellRenderer.initialize>.
*
* Parameters:
*
* cell - <mxCell> for which a new <mxCellState> should be created.
*/
mxGraphView.prototype.createState = function(cell)
{
return new mxCellState(this, cell, this.graph.getCellStyle(cell));
};
/**
* Function: getCanvas
*
* Returns the DOM node that contains the background-, draw- and
* overlay- and decoratorpanes.
*/
mxGraphView.prototype.getCanvas = function()
{
return this.canvas;
};
/**
* Function: getBackgroundPane
*
* Returns the DOM node that represents the background layer.
*/
mxGraphView.prototype.getBackgroundPane = function()
{
return this.backgroundPane;
};
/**
* Function: getDrawPane
*
* Returns the DOM node that represents the main drawing layer.
*/
mxGraphView.prototype.getDrawPane = function()
{
return this.drawPane;
};
/**
* Function: getOverlayPane
*
* Returns the DOM node that represents the layer above the drawing layer.
*/
mxGraphView.prototype.getOverlayPane = function()
{
return this.overlayPane;
};
/**
* Function: getDecoratorPane
*
* Returns the DOM node that represents the topmost drawing layer.
*/
mxGraphView.prototype.getDecoratorPane = function()
{
return this.decoratorPane;
};
/**
* Function: isContainerEvent
*
* Returns true if the event origin is one of the drawing panes or
* containers of the view.
*/
mxGraphView.prototype.isContainerEvent = function(evt)
{
var source = mxEvent.getSource(evt);
return (source == this.graph.container ||
source.parentNode == this.backgroundPane ||
(source.parentNode != null &&
source.parentNode.parentNode == this.backgroundPane) ||
source == this.canvas.parentNode ||
source == this.canvas ||
source == this.backgroundPane ||
source == this.drawPane ||
source == this.overlayPane ||
source == this.decoratorPane);
};
/**
* Function: isScrollEvent
*
* Returns true if the event origin is one of the scrollbars of the
* container in IE. Such events are ignored.
*/
mxGraphView.prototype.isScrollEvent = function(evt)
{
var offset = mxUtils.getOffset(this.graph.container);
var pt = new mxPoint(evt.clientX - offset.x, evt.clientY - offset.y);
var outWidth = this.graph.container.offsetWidth;
var inWidth = this.graph.container.clientWidth;
if (outWidth > inWidth && pt.x > inWidth + 2 && pt.x <= outWidth)
{
return true;
}
var outHeight = this.graph.container.offsetHeight;
var inHeight = this.graph.container.clientHeight;
if (outHeight > inHeight && pt.y > inHeight + 2 && pt.y <= outHeight)
{
return true;
}
return false;
};
/**
* Function: init
*
* Initializes the graph event dispatch loop for the specified container
* and invokes <create> to create the required DOM nodes for the display.
*/
mxGraphView.prototype.init = function()
{
this.installListeners();
// Creates the DOM nodes for the respective display dialect
var graph = this.graph;
if (graph.dialect == mxConstants.DIALECT_SVG)
{
this.createSvg();
}
else if (graph.dialect == mxConstants.DIALECT_VML)
{
this.createVml();
}
else
{
this.createHtml();
}
};
/**
* Function: installListeners
*
* Installs the required listeners in the container.
*/
mxGraphView.prototype.installListeners = function()
{
var graph = this.graph;
var container = graph.container;
if (container != null)
{
// Support for touch device gestures (eg. pinch to zoom)
// Double-tap handling is implemented in mxGraph.fireMouseEvent
if (mxClient.IS_TOUCH)
{
mxEvent.addListener(container, 'gesturestart', mxUtils.bind(this, function(evt)
{
graph.fireGestureEvent(evt);
mxEvent.consume(evt);
}));
mxEvent.addListener(container, 'gesturechange', mxUtils.bind(this, function(evt)
{
graph.fireGestureEvent(evt);
mxEvent.consume(evt);
}));
mxEvent.addListener(container, 'gestureend', mxUtils.bind(this, function(evt)
{
graph.fireGestureEvent(evt);
mxEvent.consume(evt);
}));
}
// Adds basic listeners for graph event dispatching
mxEvent.addGestureListeners(container, mxUtils.bind(this, function(evt)
{
// Condition to avoid scrollbar events starting a rubberband selection
if (this.isContainerEvent(evt) && ((!mxClient.IS_IE && !mxClient.IS_IE11 && !mxClient.IS_GC &&
!mxClient.IS_OP && !mxClient.IS_SF) || !this.isScrollEvent(evt)))
{
graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isContainerEvent(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
}
}),
mxUtils.bind(this, function(evt)
{
if (this.isContainerEvent(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
}
}));
// Adds listener for double click handling on background, this does always
// use native event handler, we assume that the DOM of the background
// does not change during the double click
mxEvent.addListener(container, 'dblclick', mxUtils.bind(this, function(evt)
{
if (this.isContainerEvent(evt))
{
graph.dblClick(evt);
}
}));
// Workaround for touch events which started on some DOM node
// on top of the container, in which case the cells under the
// mouse for the move and up events are not detected.
var getState = function(evt)
{
var state = null;
// Workaround for touch events which started on some DOM node
// on top of the container, in which case the cells under the
// mouse for the move and up events are not detected.
if (mxClient.IS_TOUCH)
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
// Dispatches the drop event to the graph which
// consumes and executes the source function
var pt = mxUtils.convertPoint(container, x, y);
state = graph.view.getState(graph.getCellAt(pt.x, pt.y));
}
return state;
};
// Adds basic listeners for graph event dispatching outside of the
// container and finishing the handling of a single gesture
// Implemented via graph event dispatch loop to avoid duplicate events
// in Firefox and Chrome
graph.addMouseListener(
{
mouseDown: function(sender, me)
{
graph.popupMenuHandler.hideMenu();
},
mouseMove: function() { },
mouseUp: function() { }
});
this.moveHandler = mxUtils.bind(this, function(evt)
{
// Hides the tooltip if mouse is outside container
if (graph.tooltipHandler != null && graph.tooltipHandler.isHideOnHover())
{
graph.tooltipHandler.hide();
}
if (this.captureDocumentGesture && graph.isMouseDown && graph.container != null &&
!this.isContainerEvent(evt) && graph.container.style.display != 'none' &&
graph.container.style.visibility != 'hidden' && !mxEvent.isConsumed(evt))
{
graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
}
});
this.endHandler = mxUtils.bind(this, function(evt)
{
if (this.captureDocumentGesture && graph.isMouseDown && graph.container != null &&
!this.isContainerEvent(evt) && graph.container.style.display != 'none' &&
graph.container.style.visibility != 'hidden')
{
graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
}
});
mxEvent.addGestureListeners(document, null, this.moveHandler, this.endHandler);
}
};
/**
* Function: create
*
* Creates the DOM nodes for the HTML display.
*/
mxGraphView.prototype.createHtml = function()
{
var container = this.graph.container;
if (container != null)
{
this.canvas = this.createHtmlPane('100%', '100%');
this.canvas.style.overflow = 'hidden';
// Uses minimal size for inner DIVs on Canvas. This is required
// for correct event processing in IE. If we have an overlapping
// DIV then the events on the cells are only fired for labels.
this.backgroundPane = this.createHtmlPane('1px', '1px');
this.drawPane = this.createHtmlPane('1px', '1px');
this.overlayPane = this.createHtmlPane('1px', '1px');
this.decoratorPane = this.createHtmlPane('1px', '1px');
this.canvas.appendChild(this.backgroundPane);
this.canvas.appendChild(this.drawPane);
this.canvas.appendChild(this.overlayPane);
this.canvas.appendChild(this.decoratorPane);
container.appendChild(this.canvas);
this.updateContainerStyle(container);
// Implements minWidth/minHeight in quirks mode
if (mxClient.IS_QUIRKS)
{
var onResize = mxUtils.bind(this, function(evt)
{
var bounds = this.getGraphBounds();
var width = bounds.x + bounds.width + this.graph.border;
var height = bounds.y + bounds.height + this.graph.border;
this.updateHtmlCanvasSize(width, height);
});
mxEvent.addListener(window, 'resize', onResize);
}
}
};
/**
* Function: updateHtmlCanvasSize
*
* Updates the size of the HTML canvas.
*/
mxGraphView.prototype.updateHtmlCanvasSize = function(width, height)
{
if (this.graph.container != null)
{
var ow = this.graph.container.offsetWidth;
var oh = this.graph.container.offsetHeight;
if (ow < width)
{
this.canvas.style.width = width + 'px';
}
else
{
this.canvas.style.width = '100%';
}
if (oh < height)
{
this.canvas.style.height = height + 'px';
}
else
{
this.canvas.style.height = '100%';
}
}
};
/**
* Function: createHtmlPane
*
* Creates and returns a drawing pane in HTML (DIV).
*/
mxGraphView.prototype.createHtmlPane = function(width, height)
{
var pane = document.createElement('DIV');
if (width != null && height != null)
{
pane.style.position = 'absolute';
pane.style.left = '0px';
pane.style.top = '0px';
pane.style.width = width;
pane.style.height = height;
}
else
{
pane.style.position = 'relative';
}
return pane;
};
/**
* Function: create
*
* Creates the DOM nodes for the VML display.
*/
mxGraphView.prototype.createVml = function()
{
var container = this.graph.container;
if (container != null)
{
var width = container.offsetWidth;
var height = container.offsetHeight;
this.canvas = this.createVmlPane(width, height);
this.canvas.style.overflow = 'hidden';
this.backgroundPane = this.createVmlPane(width, height);
this.drawPane = this.createVmlPane(width, height);
this.overlayPane = this.createVmlPane(width, height);
this.decoratorPane = this.createVmlPane(width, height);
this.canvas.appendChild(this.backgroundPane);
this.canvas.appendChild(this.drawPane);
this.canvas.appendChild(this.overlayPane);
this.canvas.appendChild(this.decoratorPane);
container.appendChild(this.canvas);
}
};
/**
* Function: createVmlPane
*
* Creates a drawing pane in VML (group).
*/
mxGraphView.prototype.createVmlPane = function(width, height)
{
var pane = document.createElement(mxClient.VML_PREFIX + ':group');
// At this point the width and height are potentially
// uninitialized. That's OK.
pane.style.position = 'absolute';
pane.style.left = '0px';
pane.style.top = '0px';
pane.style.width = width + 'px';
pane.style.height = height + 'px';
pane.setAttribute('coordsize', width + ',' + height);
pane.setAttribute('coordorigin', '0,0');
return pane;
};
/**
* Function: create
*
* Creates and returns the DOM nodes for the SVG display.
*/
mxGraphView.prototype.createSvg = function()
{
var container = this.graph.container;
this.canvas = document.createElementNS(mxConstants.NS_SVG, 'g');
// For background image
this.backgroundPane = document.createElementNS(mxConstants.NS_SVG, 'g');
this.canvas.appendChild(this.backgroundPane);
// Adds two layers (background is early feature)
this.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g');
this.canvas.appendChild(this.drawPane);
this.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g');
this.canvas.appendChild(this.overlayPane);
this.decoratorPane = document.createElementNS(mxConstants.NS_SVG, 'g');
this.canvas.appendChild(this.decoratorPane);
var root = document.createElementNS(mxConstants.NS_SVG, 'svg');
root.style.left = '0px';
root.style.top = '0px';
root.style.width = '100%';
root.style.height = '100%';
// NOTE: In standards mode, the SVG must have block layout
// in order for the container DIV to not show scrollbars.
root.style.display = 'block';
root.appendChild(this.canvas);
// Workaround for scrollbars in IE11 and below
if (mxClient.IS_IE || mxClient.IS_IE11)
{
root.style.overflow = 'hidden';
}
if (container != null)
{
container.appendChild(root);
this.updateContainerStyle(container);
}
};
/**
* Function: updateContainerStyle
*
* Updates the style of the container after installing the SVG DOM elements.
*/
mxGraphView.prototype.updateContainerStyle = function(container)
{
// Workaround for offset of container
var style = mxUtils.getCurrentStyle(container);
if (style != null && style.position == 'static')
{
container.style.position = 'relative';
}
// Disables built-in pan and zoom in IE10 and later
if (mxClient.IS_POINTER)
{
container.style.touchAction = 'none';
}
};
/**
* Function: destroy
*
* Destroys the view and all its resources.
*/
mxGraphView.prototype.destroy = function()
{
var root = (this.canvas != null) ? this.canvas.ownerSVGElement : null;
if (root == null)
{
root = this.canvas;
}
if (root != null && root.parentNode != null)
{
this.clear(this.currentRoot, true);
mxEvent.removeGestureListeners(document, null, this.moveHandler, this.endHandler);
mxEvent.release(this.graph.container);
root.parentNode.removeChild(root);
this.moveHandler = null;
this.endHandler = null;
this.canvas = null;
this.backgroundPane = null;
this.drawPane = null;
this.overlayPane = null;
this.decoratorPane = null;
}
};
/**
* Class: mxCurrentRootChange
*
* Action to change the current root in a view.
*
* Constructor: mxCurrentRootChange
*
* Constructs a change of the current root in the given view.
*/
function mxCurrentRootChange(view, root)
{
this.view = view;
this.root = root;
this.previous = root;
this.isUp = root == null;
if (!this.isUp)
{
var tmp = this.view.currentRoot;
var model = this.view.graph.getModel();
while (tmp != null)
{
if (tmp == root)
{
this.isUp = true;
break;
}
tmp = model.getParent(tmp);
}
}
};
/**
* Function: execute
*
* Changes the current root of the view.
*/
mxCurrentRootChange.prototype.execute = function()
{
var tmp = this.view.currentRoot;
this.view.currentRoot = this.previous;
this.previous = tmp;
var translate = this.view.graph.getTranslateForRoot(this.view.currentRoot);
if (translate != null)
{
this.view.translate = new mxPoint(-translate.x, -translate.y);
}
if (this.isUp)
{
this.view.clear(this.view.currentRoot, true);
this.view.validate();
}
else
{
this.view.refresh();
}
var name = (this.isUp) ? mxEvent.UP : mxEvent.DOWN;
this.view.fireEvent(new mxEventObject(name,
'root', this.view.currentRoot, 'previous', this.previous));
this.isUp = !this.isUp;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraph
*
* Extends <mxEventSource> to implement a graph component for
* the browser. This is the main class of the package. To activate
* panning and connections use <setPanning> and <setConnectable>.
* For rubberband selection you must create a new instance of
* <mxRubberband>. The following listeners are added to
* <mouseListeners> by default:
*
* - <tooltipHandler>: <mxTooltipHandler> that displays tooltips
* - <panningHandler>: <mxPanningHandler> for panning and popup menus
* - <connectionHandler>: <mxConnectionHandler> for creating connections
* - <graphHandler>: <mxGraphHandler> for moving and cloning cells
*
* These listeners will be called in the above order if they are enabled.
*
* Background Images:
*
* To display a background image, set the image, image width and
* image height using <setBackgroundImage>. If one of the
* above values has changed then the <view>'s <mxGraphView.validate>
* should be invoked.
*
* Cell Images:
*
* To use images in cells, a shape must be specified in the default
* vertex style (or any named style). Possible shapes are
* <mxConstants.SHAPE_IMAGE> and <mxConstants.SHAPE_LABEL>.
* The code to change the shape used in the default vertex style,
* the following code is used:
*
* (code)
* var style = graph.getStylesheet().getDefaultVertexStyle();
* style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_IMAGE;
* (end)
*
* For the default vertex style, the image to be displayed can be
* specified in a cell's style using the <mxConstants.STYLE_IMAGE>
* key and the image URL as a value, for example:
*
* (code)
* image=http://www.example.com/image.gif
* (end)
*
* For a named style, the the stylename must be the first element
* of the cell style:
*
* (code)
* stylename;image=http://www.example.com/image.gif
* (end)
*
* A cell style can have any number of key=value pairs added, divided
* by a semicolon as follows:
*
* (code)
* [stylename;|key=value;]
* (end)
*
* Labels:
*
* The cell labels are defined by <getLabel> which uses <convertValueToString>
* if <labelsVisible> is true. If a label must be rendered as HTML markup, then
* <isHtmlLabel> should return true for the respective cell. If all labels
* contain HTML markup, <htmlLabels> can be set to true. NOTE: Enabling HTML
* labels carries a possible security risk (see the section on security in
* the manual).
*
* If wrapping is needed for a label, then <isHtmlLabel> and <isWrapping> must
* return true for the cell whose label should be wrapped. See <isWrapping> for
* an example.
*
* If clipping is needed to keep the rendering of a HTML label inside the
* bounds of its vertex, then <isClipping> should return true for the
* respective cell.
*
* By default, edge labels are movable and vertex labels are fixed. This can be
* changed by setting <edgeLabelsMovable> and <vertexLabelsMovable>, or by
* overriding <isLabelMovable>.
*
* In-place Editing:
*
* In-place editing is started with a doubleclick or by typing F2.
* Programmatically, <edit> is used to check if the cell is editable
* (<isCellEditable>) and call <startEditingAtCell>, which invokes
* <mxCellEditor.startEditing>. The editor uses the value returned
* by <getEditingValue> as the editing value.
*
* After in-place editing, <labelChanged> is called, which invokes
* <mxGraphModel.setValue>, which in turn calls
* <mxGraphModel.valueForCellChanged> via <mxValueChange>.
*
* The event that triggers in-place editing is passed through to the
* <cellEditor>, which may take special actions depending on the type of the
* event or mouse location, and is also passed to <getEditingValue>. The event
* is then passed back to the event processing functions which can perform
* specific actions based on the trigger event.
*
* Tooltips:
*
* Tooltips are implemented by <getTooltip>, which calls <getTooltipForCell>
* if a cell is under the mousepointer. The default implementation checks if
* the cell has a getTooltip function and calls it if it exists. Hence, in order
* to provide custom tooltips, the cell must provide a getTooltip function, or
* one of the two above functions must be overridden.
*
* Typically, for custom cell tooltips, the latter function is overridden as
* follows:
*
* (code)
* graph.getTooltipForCell = function(cell)
* {
* var label = this.convertValueToString(cell);
* return 'Tooltip for '+label;
* }
* (end)
*
* When using a config file, the function is overridden in the mxGraph section
* using the following entry:
*
* (code)
* <add as="getTooltipForCell"><![CDATA[
* function(cell)
* {
* var label = this.convertValueToString(cell);
* return 'Tooltip for '+label;
* }
* ]]></add>
* (end)
*
* "this" refers to the graph in the implementation, so for example to check if
* a cell is an edge, you use this.getModel().isEdge(cell)
*
* For replacing the default implementation of <getTooltipForCell> (rather than
* replacing the function on a specific instance), the following code should be
* used after loading the JavaScript files, but before creating a new mxGraph
* instance using <mxGraph>:
*
* (code)
* mxGraph.prototype.getTooltipForCell = function(cell)
* {
* var label = this.convertValueToString(cell);
* return 'Tooltip for '+label;
* }
* (end)
*
* Shapes & Styles:
*
* The implementation of new shapes is demonstrated in the examples. We'll assume
* that we have implemented a custom shape with the name BoxShape which we want
* to use for drawing vertices. To use this shape, it must first be registered in
* the cell renderer as follows:
*
* (code)
* mxCellRenderer.registerShape('box', BoxShape);
* (end)
*
* The code registers the BoxShape constructor under the name box in the cell
* renderer of the graph. The shape can now be referenced using the shape-key in
* a style definition. (The cell renderer contains a set of additional shapes,
* namely one for each constant with a SHAPE-prefix in <mxConstants>.)
*
* Styles are a collection of key, value pairs and a stylesheet is a collection
* of named styles. The names are referenced by the cellstyle, which is stored
* in <mxCell.style> with the following format: [stylename;|key=value;]. The
* string is resolved to a collection of key, value pairs, where the keys are
* overridden with the values in the string.
*
* When introducing a new shape, the name under which the shape is registered
* must be used in the stylesheet. There are three ways of doing this:
*
* - By changing the default style, so that all vertices will use the new
* shape
* - By defining a new style, so that only vertices with the respective
* cellstyle will use the new shape
* - By using shape=box in the cellstyle's optional list of key, value pairs
* to be overridden
*
* In the first case, the code to fetch and modify the default style for
* vertices is as follows:
*
* (code)
* var style = graph.getStylesheet().getDefaultVertexStyle();
* style[mxConstants.STYLE_SHAPE] = 'box';
* (end)
*
* The code takes the default vertex style, which is used for all vertices that
* do not have a specific cellstyle, and modifies the value for the shape-key
* in-place to use the new BoxShape for drawing vertices. This is done by
* assigning the box value in the second line, which refers to the name of the
* BoxShape in the cell renderer.
*
* In the second case, a collection of key, value pairs is created and then
* added to the stylesheet under a new name. In order to distinguish the
* shapename and the stylename we'll use boxstyle for the stylename:
*
* (code)
* var style = new Object();
* style[mxConstants.STYLE_SHAPE] = 'box';
* style[mxConstants.STYLE_STROKECOLOR] = '#000000';
* style[mxConstants.STYLE_FONTCOLOR] = '#000000';
* graph.getStylesheet().putCellStyle('boxstyle', style);
* (end)
*
* The code adds a new style with the name boxstyle to the stylesheet. To use
* this style with a cell, it must be referenced from the cellstyle as follows:
*
* (code)
* var vertex = graph.insertVertex(parent, null, 'Hello, World!', 20, 20, 80, 20,
* 'boxstyle');
* (end)
*
* To summarize, each new shape must be registered in the <mxCellRenderer> with
* a unique name. That name is then used as the value of the shape-key in a
* default or custom style. If there are multiple custom shapes, then there
* should be a separate style for each shape.
*
* Inheriting Styles:
*
* For fill-, stroke-, gradient- and indicatorColors special keywords can be
* used. The inherit keyword for one of these colors will inherit the color
* for the same key from the parent cell. The swimlane keyword does the same,
* but inherits from the nearest swimlane in the ancestor hierarchy. Finally,
* the indicated keyword will use the color of the indicator as the color for
* the given key.
*
* Scrollbars:
*
* The <containers> overflow CSS property defines if scrollbars are used to
* display the graph. For values of 'auto' or 'scroll', the scrollbars will
* be shown. Note that the <resizeContainer> flag is normally not used
* together with scrollbars, as it will resize the container to match the
* size of the graph after each change.
*
* Multiplicities and Validation:
*
* To control the possible connections in mxGraph, <getEdgeValidationError> is
* used. The default implementation of the function uses <multiplicities>,
* which is an array of <mxMultiplicity>. Using this class allows to establish
* simple multiplicities, which are enforced by the graph.
*
* The <mxMultiplicity> uses <mxCell.is> to determine for which terminals it
* applies. The default implementation of <mxCell.is> works with DOM nodes (XML
* nodes) and checks if the given type parameter matches the nodeName of the
* node (case insensitive). Optionally, an attributename and value can be
* specified which are also checked.
*
* <getEdgeValidationError> is called whenever the connectivity of an edge
* changes. It returns an empty string or an error message if the edge is
* invalid or null if the edge is valid. If the returned string is not empty
* then it is displayed as an error message.
*
* <mxMultiplicity> allows to specify the multiplicity between a terminal and
* its possible neighbors. For example, if any rectangle may only be connected
* to, say, a maximum of two circles you can add the following rule to
* <multiplicities>:
*
* (code)
* graph.multiplicities.push(new mxMultiplicity(
* true, 'rectangle', null, null, 0, 2, ['circle'],
* 'Only 2 targets allowed',
* 'Only shape targets allowed'));
* (end)
*
* This will display the first error message whenever a rectangle is connected
* to more than two circles and the second error message if a rectangle is
* connected to anything but a circle.
*
* For certain multiplicities, such as a minimum of 1 connection, which cannot
* be enforced at cell creation time (unless the cell is created together with
* the connection), mxGraph offers <validate> which checks all multiplicities
* for all cells and displays the respective error messages in an overlay icon
* on the cells.
*
* If a cell is collapsed and contains validation errors, a respective warning
* icon is attached to the collapsed cell.
*
* Auto-Layout:
*
* For automatic layout, the <getLayout> hook is provided in <mxLayoutManager>.
* It can be overridden to return a layout algorithm for the children of a
* given cell.
*
* Unconnected edges:
*
* The default values for all switches are designed to meet the requirements of
* general diagram drawing applications. A very typical set of settings to
* avoid edges that are not connected is the following:
*
* (code)
* graph.setAllowDanglingEdges(false);
* graph.setDisconnectOnMove(false);
* (end)
*
* Setting the <cloneInvalidEdges> switch to true is optional. This switch
* controls if edges are inserted after a copy, paste or clone-drag if they are
* invalid. For example, edges are invalid if copied or control-dragged without
* having selected the corresponding terminals and allowDanglingEdges is
* false, in which case the edges will not be cloned if the switch is false.
*
* Output:
*
* To produce an XML representation for a diagram, the following code can be
* used.
*
* (code)
* var enc = new mxCodec(mxUtils.createXmlDocument());
* var node = enc.encode(graph.getModel());
* (end)
*
* This will produce an XML node than can be handled using the DOM API or
* turned into a string representation using the following code:
*
* (code)
* var xml = mxUtils.getXml(node);
* (end)
*
* To obtain a formatted string, mxUtils.getPrettyXml can be used instead.
*
* This string can now be stored in a local persistent storage (for example
* using Google Gears) or it can be passed to a backend using mxUtils.post as
* follows. The url variable is the URL of the Java servlet, PHP page or HTTP
* handler, depending on the server.
*
* (code)
* var xmlString = encodeURIComponent(mxUtils.getXml(node));
* mxUtils.post(url, 'xml='+xmlString, function(req)
* {
* // Process server response using req of type mxXmlRequest
* });
* (end)
*
* Input:
*
* To load an XML representation of a diagram into an existing graph object
* mxUtils.load can be used as follows. The url variable is the URL of the Java
* servlet, PHP page or HTTP handler that produces the XML string.
*
* (code)
* var xmlDoc = mxUtils.load(url).getXml();
* var node = xmlDoc.documentElement;
* var dec = new mxCodec(node.ownerDocument);
* dec.decode(node, graph.getModel());
* (end)
*
* For creating a page that loads the client and a diagram using a single
* request please refer to the deployment examples in the backends.
*
* Functional dependencies:
*
* (see images/callgraph.png)
*
* Resources:
*
* resources/graph - Language resources for mxGraph
*
* Group: Events
*
* Event: mxEvent.ROOT
*
* Fires if the root in the model has changed. This event has no properties.
*
* Event: mxEvent.ALIGN_CELLS
*
* Fires between begin- and endUpdate in <alignCells>. The <code>cells</code>
* and <code>align</code> properties contain the respective arguments that were
* passed to <alignCells>.
*
* Event: mxEvent.FLIP_EDGE
*
* Fires between begin- and endUpdate in <flipEdge>. The <code>edge</code>
* property contains the edge passed to <flipEdge>.
*
* Event: mxEvent.ORDER_CELLS
*
* Fires between begin- and endUpdate in <orderCells>. The <code>cells</code>
* and <code>back</code> properties contain the respective arguments that were
* passed to <orderCells>.
*
* Event: mxEvent.CELLS_ORDERED
*
* Fires between begin- and endUpdate in <cellsOrdered>. The <code>cells</code>
* and <code>back</code> arguments contain the respective arguments that were
* passed to <cellsOrdered>.
*
* Event: mxEvent.GROUP_CELLS
*
* Fires between begin- and endUpdate in <groupCells>. The <code>group</code>,
* <code>cells</code> and <code>border</code> arguments contain the respective
* arguments that were passed to <groupCells>.
*
* Event: mxEvent.UNGROUP_CELLS
*
* Fires between begin- and endUpdate in <ungroupCells>. The <code>cells</code>
* property contains the array of cells that was passed to <ungroupCells>.
*
* Event: mxEvent.REMOVE_CELLS_FROM_PARENT
*
* Fires between begin- and endUpdate in <removeCellsFromParent>. The
* <code>cells</code> property contains the array of cells that was passed to
* <removeCellsFromParent>.
*
* Event: mxEvent.ADD_CELLS
*
* Fires between begin- and endUpdate in <addCells>. The <code>cells</code>,
* <code>parent</code>, <code>index</code>, <code>source</code> and
* <code>target</code> properties contain the respective arguments that were
* passed to <addCells>.
*
* Event: mxEvent.CELLS_ADDED
*
* Fires between begin- and endUpdate in <cellsAdded>. The <code>cells</code>,
* <code>parent</code>, <code>index</code>, <code>source</code>,
* <code>target</code> and <code>absolute</code> properties contain the
* respective arguments that were passed to <cellsAdded>.
*
* Event: mxEvent.REMOVE_CELLS
*
* Fires between begin- and endUpdate in <removeCells>. The <code>cells</code>
* and <code>includeEdges</code> arguments contain the respective arguments
* that were passed to <removeCells>.
*
* Event: mxEvent.CELLS_REMOVED
*
* Fires between begin- and endUpdate in <cellsRemoved>. The <code>cells</code>
* argument contains the array of cells that was removed.
*
* Event: mxEvent.SPLIT_EDGE
*
* Fires between begin- and endUpdate in <splitEdge>. The <code>edge</code>
* property contains the edge to be splitted, the <code>cells</code>,
* <code>newEdge</code>, <code>dx</code> and <code>dy</code> properties contain
* the respective arguments that were passed to <splitEdge>.
*
* Event: mxEvent.TOGGLE_CELLS
*
* Fires between begin- and endUpdate in <toggleCells>. The <code>show</code>,
* <code>cells</code> and <code>includeEdges</code> properties contain the
* respective arguments that were passed to <toggleCells>.
*
* Event: mxEvent.FOLD_CELLS
*
* Fires between begin- and endUpdate in <foldCells>. The
* <code>collapse</code>, <code>cells</code> and <code>recurse</code>
* properties contain the respective arguments that were passed to <foldCells>.
*
* Event: mxEvent.CELLS_FOLDED
*
* Fires between begin- and endUpdate in cellsFolded. The
* <code>collapse</code>, <code>cells</code> and <code>recurse</code>
* properties contain the respective arguments that were passed to
* <cellsFolded>.
*
* Event: mxEvent.UPDATE_CELL_SIZE
*
* Fires between begin- and endUpdate in <updateCellSize>. The
* <code>cell</code> and <code>ignoreChildren</code> properties contain the
* respective arguments that were passed to <updateCellSize>.
*
* Event: mxEvent.RESIZE_CELLS
*
* Fires between begin- and endUpdate in <resizeCells>. The <code>cells</code>
* and <code>bounds</code> properties contain the respective arguments that
* were passed to <resizeCells>.
*
* Event: mxEvent.CELLS_RESIZED
*
* Fires between begin- and endUpdate in <cellsResized>. The <code>cells</code>
* and <code>bounds</code> properties contain the respective arguments that
* were passed to <cellsResized>.
*
* Event: mxEvent.MOVE_CELLS
*
* Fires between begin- and endUpdate in <moveCells>. The <code>cells</code>,
* <code>dx</code>, <code>dy</code>, <code>clone</code>, <code>target</code>
* and <code>event</code> properties contain the respective arguments that
* were passed to <moveCells>.
*
* Event: mxEvent.CELLS_MOVED
*
* Fires between begin- and endUpdate in <cellsMoved>. The <code>cells</code>,
* <code>dx</code>, <code>dy</code> and <code>disconnect</code> properties
* contain the respective arguments that were passed to <cellsMoved>.
*
* Event: mxEvent.CONNECT_CELL
*
* Fires between begin- and endUpdate in <connectCell>. The <code>edge</code>,
* <code>terminal</code> and <code>source</code> properties contain the
* respective arguments that were passed to <connectCell>.
*
* Event: mxEvent.CELL_CONNECTED
*
* Fires between begin- and endUpdate in <cellConnected>. The
* <code>edge</code>, <code>terminal</code> and <code>source</code> properties
* contain the respective arguments that were passed to <cellConnected>.
*
* Event: mxEvent.REFRESH
*
* Fires after <refresh> was executed. This event has no properties.
*
* Event: mxEvent.CLICK
*
* Fires in <click> after a click event. The <code>event</code> property
* contains the original mouse event and <code>cell</code> property contains
* the cell under the mouse or null if the background was clicked.
*
* Event: mxEvent.DOUBLE_CLICK
*
* Fires in <dblClick> after a double click. The <code>event</code> property
* contains the original mouse event and the <code>cell</code> property
* contains the cell under the mouse or null if the background was clicked.
*
* Event: mxEvent.GESTURE
*
* Fires in <fireGestureEvent> after a touch gesture. The <code>event</code>
* property contains the original gesture end event and the <code>cell</code>
* property contains the optional cell associated with the gesture.
*
* Event: mxEvent.TAP_AND_HOLD
*
* Fires in <tapAndHold> if a tap and hold event was detected. The <code>event</code>
* property contains the initial touch event and the <code>cell</code> property
* contains the cell under the mouse or null if the background was clicked.
*
* Event: mxEvent.FIRE_MOUSE_EVENT
*
* Fires in <fireMouseEvent> before the mouse listeners are invoked. The
* <code>eventName</code> property contains the event name and the
* <code>event</code> property contains the <mxMouseEvent>.
*
* Event: mxEvent.SIZE
*
* Fires after <sizeDidChange> was executed. The <code>bounds</code> property
* contains the new graph bounds.
*
* Event: mxEvent.START_EDITING
*
* Fires before the in-place editor starts in <startEditingAtCell>. The
* <code>cell</code> property contains the cell that is being edited and the
* <code>event</code> property contains the optional event argument that was
* passed to <startEditingAtCell>.
*
* Event: mxEvent.EDITING_STARTED
*
* Fires after the in-place editor starts in <startEditingAtCell>. The
* <code>cell</code> property contains the cell that is being edited and the
* <code>event</code> property contains the optional event argument that was
* passed to <startEditingAtCell>.
*
* Event: mxEvent.EDITING_STOPPED
*
* Fires after the in-place editor stops in <stopEditing>.
*
* Event: mxEvent.LABEL_CHANGED
*
* Fires between begin- and endUpdate in <cellLabelChanged>. The
* <code>cell</code> property contains the cell, the <code>value</code>
* property contains the new value for the cell, the <code>old</code> property
* contains the old value and the optional <code>event</code> property contains
* the mouse event that started the edit.
*
* Event: mxEvent.ADD_OVERLAY
*
* Fires after an overlay is added in <addCellOverlay>. The <code>cell</code>
* property contains the cell and the <code>overlay</code> property contains
* the <mxCellOverlay> that was added.
*
* Event: mxEvent.REMOVE_OVERLAY
*
* Fires after an overlay is removed in <removeCellOverlay> and
* <removeCellOverlays>. The <code>cell</code> property contains the cell and
* the <code>overlay</code> property contains the <mxCellOverlay> that was
* removed.
*
* Constructor: mxGraph
*
* Constructs a new mxGraph in the specified container. Model is an optional
* mxGraphModel. If no model is provided, a new mxGraphModel instance is
* used as the model. The container must have a valid owner document prior
* to calling this function in Internet Explorer. RenderHint is a string to
* affect the display performance and rendering in IE, but not in SVG-based
* browsers. The parameter is mapped to <dialect>, which may
* be one of <mxConstants.DIALECT_SVG> for SVG-based browsers,
* <mxConstants.DIALECT_STRICTHTML> for fastest display mode,
* <mxConstants.DIALECT_PREFERHTML> for faster display mode,
* <mxConstants.DIALECT_MIXEDHTML> for fast and <mxConstants.DIALECT_VML>
* for exact display mode (slowest). The dialects are defined in mxConstants.
* The default values are DIALECT_SVG for SVG-based browsers and
* DIALECT_MIXED for IE.
*
* The possible values for the renderingHint parameter are explained below:
*
* fast - The parameter is based on the fact that the display performance is
* highly improved in IE if the VML is not contained within a VML group
* element. The lack of a group element only slightly affects the display while
* panning, but improves the performance by almost a factor of 2, while keeping
* the display sufficiently accurate. This also allows to render certain shapes as HTML
* if the display accuracy is not affected, which is implemented by
* <mxShape.isMixedModeHtml>. This is the default setting and is mapped to
* DIALECT_MIXEDHTML.
* faster - Same as fast, but more expensive shapes are avoided. This is
* controlled by <mxShape.preferModeHtml>. The default implementation will
* avoid gradients and rounded rectangles, but more significant shapes, such
* as rhombus, ellipse, actor and cylinder will be rendered accurately. This
* setting is mapped to DIALECT_PREFERHTML.
* fastest - Almost anything will be rendered in Html. This allows for
* rectangles, labels and images. This setting is mapped to
* DIALECT_STRICTHTML.
* exact - If accurate panning is required and if the diagram is small (up
* to 100 cells), then this value should be used. In this mode, a group is
* created that contains the VML. This allows for accurate panning and is
* mapped to DIALECT_VML.
*
* Example:
*
* To create a graph inside a DOM node with an id of graph:
* (code)
* var container = document.getElementById('graph');
* var graph = new mxGraph(container);
* (end)
*
* Parameters:
*
* container - Optional DOM node that acts as a container for the graph.
* If this is null then the container can be initialized later using
* <init>.
* model - Optional <mxGraphModel> that constitutes the graph data.
* renderHint - Optional string that specifies the display accuracy and
* performance. Default is mxConstants.DIALECT_MIXEDHTML (for IE).
* stylesheet - Optional <mxStylesheet> to be used in the graph.
*/
function mxGraph(container, model, renderHint, stylesheet)
{
// Initializes the variable in case the prototype has been
// modified to hold some listeners (which is possible because
// the createHandlers call is executed regardless of the
// arguments passed into the ctor).
this.mouseListeners = null;
// Converts the renderHint into a dialect
this.renderHint = renderHint;
if (mxClient.IS_SVG)
{
this.dialect = mxConstants.DIALECT_SVG;
}
else if (renderHint == mxConstants.RENDERING_HINT_EXACT && mxClient.IS_VML)
{
this.dialect = mxConstants.DIALECT_VML;
}
else if (renderHint == mxConstants.RENDERING_HINT_FASTEST)
{
this.dialect = mxConstants.DIALECT_STRICTHTML;
}
else if (renderHint == mxConstants.RENDERING_HINT_FASTER)
{
this.dialect = mxConstants.DIALECT_PREFERHTML;
}
else // default for VML
{
this.dialect = mxConstants.DIALECT_MIXEDHTML;
}
// Initializes the main members that do not require a container
this.model = (model != null) ? model : new mxGraphModel();
this.multiplicities = [];
this.imageBundles = [];
this.cellRenderer = this.createCellRenderer();
this.setSelectionModel(this.createSelectionModel());
this.setStylesheet((stylesheet != null) ? stylesheet : this.createStylesheet());
this.view = this.createGraphView();
// Adds a graph model listener to update the view
this.graphModelChangeListener = mxUtils.bind(this, function(sender, evt)
{
this.graphModelChanged(evt.getProperty('edit').changes);
});
this.model.addListener(mxEvent.CHANGE, this.graphModelChangeListener);
// Installs basic event handlers with disabled default settings.
this.createHandlers();
// Initializes the display if a container was specified
if (container != null)
{
this.init(container);
}
this.view.revalidate();
};
/**
* Installs the required language resources at class
* loading time.
*/
if (mxLoadResources)
{
mxResources.add(mxClient.basePath + '/resources/graph');
}
else
{
mxClient.defaultBundles.push(mxClient.basePath + '/resources/graph');
}
/**
* Extends mxEventSource.
*/
mxGraph.prototype = new mxEventSource();
mxGraph.prototype.constructor = mxGraph;
/**
* Group: Variables
*/
/**
* Variable: mouseListeners
*
* Holds the mouse event listeners. See <fireMouseEvent>.
*/
mxGraph.prototype.mouseListeners = null;
/**
* Variable: isMouseDown
*
* Holds the state of the mouse button.
*/
mxGraph.prototype.isMouseDown = false;
/**
* Variable: model
*
* Holds the <mxGraphModel> that contains the cells to be displayed.
*/
mxGraph.prototype.model = null;
/**
* Variable: view
*
* Holds the <mxGraphView> that caches the <mxCellStates> for the cells.
*/
mxGraph.prototype.view = null;
/**
* Variable: stylesheet
*
* Holds the <mxStylesheet> that defines the appearance of the cells.
*
*
* Example:
*
* Use the following code to read a stylesheet into an existing graph.
*
* (code)
* var req = mxUtils.load('stylesheet.xml');
* var root = req.getDocumentElement();
* var dec = new mxCodec(root.ownerDocument);
* dec.decode(root, graph.stylesheet);
* (end)
*/
mxGraph.prototype.stylesheet = null;
/**
* Variable: selectionModel
*
* Holds the <mxGraphSelectionModel> that models the current selection.
*/
mxGraph.prototype.selectionModel = null;
/**
* Variable: cellEditor
*
* Holds the <mxCellEditor> that is used as the in-place editing.
*/
mxGraph.prototype.cellEditor = null;
/**
* Variable: cellRenderer
*
* Holds the <mxCellRenderer> for rendering the cells in the graph.
*/
mxGraph.prototype.cellRenderer = null;
/**
* Variable: multiplicities
*
* An array of <mxMultiplicities> describing the allowed
* connections in a graph.
*/
mxGraph.prototype.multiplicities = null;
/**
* Variable: renderHint
*
* RenderHint as it was passed to the constructor.
*/
mxGraph.prototype.renderHint = null;
/**
* Variable: dialect
*
* Dialect to be used for drawing the graph. Possible values are all
* constants in <mxConstants> with a DIALECT-prefix.
*/
mxGraph.prototype.dialect = null;
/**
* Variable: gridSize
*
* Specifies the grid size. Default is 10.
*/
mxGraph.prototype.gridSize = 10;
/**
* Variable: gridEnabled
*
* Specifies if the grid is enabled. This is used in <snap>. Default is
* true.
*/
mxGraph.prototype.gridEnabled = true;
/**
* Variable: portsEnabled
*
* Specifies if ports are enabled. This is used in <cellConnected> to update
* the respective style. Default is true.
*/
mxGraph.prototype.portsEnabled = true;
/**
* Variable: nativeDoubleClickEnabled
*
* Specifies if native double click events should be detected. Default is true.
*/
mxGraph.prototype.nativeDblClickEnabled = true;
/**
* Variable: doubleTapEnabled
*
* Specifies if double taps on touch-based devices should be handled as a
* double click. Default is true.
*/
mxGraph.prototype.doubleTapEnabled = true;
/**
* Variable: doubleTapTimeout
*
* Specifies the timeout for double taps and non-native double clicks. Default
* is 500 ms.
*/
mxGraph.prototype.doubleTapTimeout = 500;
/**
* Variable: doubleTapTolerance
*
* Specifies the tolerance for double taps and double clicks in quirks mode.
* Default is 25 pixels.
*/
mxGraph.prototype.doubleTapTolerance = 25;
/**
* Variable: lastTouchX
*
* Holds the x-coordinate of the last touch event for double tap detection.
*/
mxGraph.prototype.lastTouchY = 0;
/**
* Variable: lastTouchX
*
* Holds the y-coordinate of the last touch event for double tap detection.
*/
mxGraph.prototype.lastTouchY = 0;
/**
* Variable: lastTouchTime
*
* Holds the time of the last touch event for double click detection.
*/
mxGraph.prototype.lastTouchTime = 0;
/**
* Variable: tapAndHoldEnabled
*
* Specifies if tap and hold should be used for starting connections on touch-based
* devices. Default is true.
*/
mxGraph.prototype.tapAndHoldEnabled = true;
/**
* Variable: tapAndHoldDelay
*
* Specifies the time for a tap and hold. Default is 500 ms.
*/
mxGraph.prototype.tapAndHoldDelay = 500;
/**
* Variable: tapAndHoldInProgress
*
* True if the timer for tap and hold events is running.
*/
mxGraph.prototype.tapAndHoldInProgress = false;
/**
* Variable: tapAndHoldValid
*
* True as long as the timer is running and the touch events
* stay within the given <tapAndHoldTolerance>.
*/
mxGraph.prototype.tapAndHoldValid = false;
/**
* Variable: initialTouchX
*
* Holds the x-coordinate of the intial touch event for tap and hold.
*/
mxGraph.prototype.initialTouchX = 0;
/**
* Variable: initialTouchY
*
* Holds the y-coordinate of the intial touch event for tap and hold.
*/
mxGraph.prototype.initialTouchY = 0;
/**
* Variable: tolerance
*
* Tolerance for a move to be handled as a single click.
* Default is 4 pixels.
*/
mxGraph.prototype.tolerance = 4;
/**
* Variable: defaultOverlap
*
* Value returned by <getOverlap> if <isAllowOverlapParent> returns
* true for the given cell. <getOverlap> is used in <constrainChild> if
* <isConstrainChild> returns true. The value specifies the
* portion of the child which is allowed to overlap the parent.
*/
mxGraph.prototype.defaultOverlap = 0.5;
/**
* Variable: defaultParent
*
* Specifies the default parent to be used to insert new cells.
* This is used in <getDefaultParent>. Default is null.
*/
mxGraph.prototype.defaultParent = null;
/**
* Variable: alternateEdgeStyle
*
* Specifies the alternate edge style to be used if the main control point
* on an edge is being doubleclicked. Default is null.
*/
mxGraph.prototype.alternateEdgeStyle = null;
/**
* Variable: backgroundImage
*
* Specifies the <mxImage> to be returned by <getBackgroundImage>. Default
* is null.
*
* Example:
*
* (code)
* var img = new mxImage('http://www.example.com/maps/examplemap.jpg', 1024, 768);
* graph.setBackgroundImage(img);
* graph.view.validate();
* (end)
*/
mxGraph.prototype.backgroundImage = null;
/**
* Variable: pageVisible
*
* Specifies if the background page should be visible. Default is false.
* Not yet implemented.
*/
mxGraph.prototype.pageVisible = false;
/**
* Variable: pageBreaksVisible
*
* Specifies if a dashed line should be drawn between multiple pages. Default
* is false. If you change this value while a graph is being displayed then you
* should call <sizeDidChange> to force an update of the display.
*/
mxGraph.prototype.pageBreaksVisible = false;
/**
* Variable: pageBreakColor
*
* Specifies the color for page breaks. Default is 'gray'.
*/
mxGraph.prototype.pageBreakColor = 'gray';
/**
* Variable: pageBreakDashed
*
* Specifies the page breaks should be dashed. Default is true.
*/
mxGraph.prototype.pageBreakDashed = true;
/**
* Variable: minPageBreakDist
*
* Specifies the minimum distance for page breaks to be visible. Default is
* 20 (in pixels).
*/
mxGraph.prototype.minPageBreakDist = 20;
/**
* Variable: preferPageSize
*
* Specifies if the graph size should be rounded to the next page number in
* <sizeDidChange>. This is only used if the graph container has scrollbars.
* Default is false.
*/
mxGraph.prototype.preferPageSize = false;
/**
* Variable: pageFormat
*
* Specifies the page format for the background page. Default is
* <mxConstants.PAGE_FORMAT_A4_PORTRAIT>. This is used as the default in
* <mxPrintPreview> and for painting the background page if <pageVisible> is
* true and the pagebreaks if <pageBreaksVisible> is true.
*/
mxGraph.prototype.pageFormat = mxConstants.PAGE_FORMAT_A4_PORTRAIT;
/**
* Variable: pageScale
*
* Specifies the scale of the background page. Default is 1.5.
* Not yet implemented.
*/
mxGraph.prototype.pageScale = 1.5;
/**
* Variable: enabled
*
* Specifies the return value for <isEnabled>. Default is true.
*/
mxGraph.prototype.enabled = true;
/**
* Variable: escapeEnabled
*
* Specifies if <mxKeyHandler> should invoke <escape> when the escape key
* is pressed. Default is true.
*/
mxGraph.prototype.escapeEnabled = true;
/**
* Variable: invokesStopCellEditing
*
* If true, when editing is to be stopped by way of selection changing,
* data in diagram changing or other means stopCellEditing is invoked, and
* changes are saved. This is implemented in a focus handler in
* <mxCellEditor>. Default is true.
*/
mxGraph.prototype.invokesStopCellEditing = true;
/**
* Variable: enterStopsCellEditing
*
* If true, pressing the enter key without pressing control or shift will stop
* editing and accept the new value. This is used in <mxCellEditor> to stop
* cell editing. Note: You can always use F2 and escape to stop editing.
* Default is false.
*/
mxGraph.prototype.enterStopsCellEditing = false;
/**
* Variable: useScrollbarsForPanning
*
* Specifies if scrollbars should be used for panning in <panGraph> if
* any scrollbars are available. If scrollbars are enabled in CSS, but no
* scrollbars appear because the graph is smaller than the container size,
* then no panning occurs if this is true. Default is true.
*/
mxGraph.prototype.useScrollbarsForPanning = true;
/**
* Variable: exportEnabled
*
* Specifies the return value for <canExportCell>. Default is true.
*/
mxGraph.prototype.exportEnabled = true;
/**
* Variable: importEnabled
*
* Specifies the return value for <canImportCell>. Default is true.
*/
mxGraph.prototype.importEnabled = true;
/**
* Variable: cellsLocked
*
* Specifies the return value for <isCellLocked>. Default is false.
*/
mxGraph.prototype.cellsLocked = false;
/**
* Variable: cellsCloneable
*
* Specifies the return value for <isCellCloneable>. Default is true.
*/
mxGraph.prototype.cellsCloneable = true;
/**
* Variable: foldingEnabled
*
* Specifies if folding (collapse and expand via an image icon in the graph
* should be enabled). Default is true.
*/
mxGraph.prototype.foldingEnabled = true;
/**
* Variable: cellsEditable
*
* Specifies the return value for <isCellEditable>. Default is true.
*/
mxGraph.prototype.cellsEditable = true;
/**
* Variable: cellsDeletable
*
* Specifies the return value for <isCellDeletable>. Default is true.
*/
mxGraph.prototype.cellsDeletable = true;
/**
* Variable: cellsMovable
*
* Specifies the return value for <isCellMovable>. Default is true.
*/
mxGraph.prototype.cellsMovable = true;
/**
* Variable: edgeLabelsMovable
*
* Specifies the return value for edges in <isLabelMovable>. Default is true.
*/
mxGraph.prototype.edgeLabelsMovable = true;
/**
* Variable: vertexLabelsMovable
*
* Specifies the return value for vertices in <isLabelMovable>. Default is false.
*/
mxGraph.prototype.vertexLabelsMovable = false;
/**
* Variable: dropEnabled
*
* Specifies the return value for <isDropEnabled>. Default is false.
*/
mxGraph.prototype.dropEnabled = false;
/**
* Variable: splitEnabled
*
* Specifies if dropping onto edges should be enabled. This is ignored if
* <dropEnabled> is false. If enabled, it will call <splitEdge> to carry
* out the drop operation. Default is true.
*/
mxGraph.prototype.splitEnabled = true;
/**
* Variable: cellsResizable
*
* Specifies the return value for <isCellResizable>. Default is true.
*/
mxGraph.prototype.cellsResizable = true;
/**
* Variable: cellsBendable
*
* Specifies the return value for <isCellsBendable>. Default is true.
*/
mxGraph.prototype.cellsBendable = true;
/**
* Variable: cellsSelectable
*
* Specifies the return value for <isCellSelectable>. Default is true.
*/
mxGraph.prototype.cellsSelectable = true;
/**
* Variable: cellsDisconnectable
*
* Specifies the return value for <isCellDisconntable>. Default is true.
*/
mxGraph.prototype.cellsDisconnectable = true;
/**
* Variable: autoSizeCells
*
* Specifies if the graph should automatically update the cell size after an
* edit. This is used in <isAutoSizeCell>. Default is false.
*/
mxGraph.prototype.autoSizeCells = false;
/**
* Variable: autoSizeCellsOnAdd
*
* Specifies if autoSize style should be applied when cells are added. Default is false.
*/
mxGraph.prototype.autoSizeCellsOnAdd = false;
/**
* Variable: autoScroll
*
* Specifies if the graph should automatically scroll if the mouse goes near
* the container edge while dragging. This is only taken into account if the
* container has scrollbars. Default is true.
*
* If you need this to work without scrollbars then set <ignoreScrollbars> to
* true. Please consult the <ignoreScrollbars> for details. In general, with
* no scrollbars, the use of <allowAutoPanning> is recommended.
*/
mxGraph.prototype.autoScroll = true;
/**
* Variable: ignoreScrollbars
*
* Specifies if the graph should automatically scroll regardless of the
* scrollbars. This will scroll the container using positive values for
* scroll positions (ie usually only rightwards and downwards). To avoid
* possible conflicts with panning, set <translateToScrollPosition> to true.
*/
mxGraph.prototype.ignoreScrollbars = false;
/**
* Variable: translateToScrollPosition
*
* Specifies if the graph should automatically convert the current scroll
* position to a translate in the graph view when a mouseUp event is received.
* This can be used to avoid conflicts when using <autoScroll> and
* <ignoreScrollbars> with no scrollbars in the container.
*/
mxGraph.prototype.translateToScrollPosition = false;
/**
* Variable: timerAutoScroll
*
* Specifies if autoscrolling should be carried out via mxPanningManager even
* if the container has scrollbars. This disables <scrollPointToVisible> and
* uses <mxPanningManager> instead. If this is true then <autoExtend> is
* disabled. It should only be used with a scroll buffer or when scollbars
* are visible and scrollable in all directions. Default is false.
*/
mxGraph.prototype.timerAutoScroll = false;
/**
* Variable: allowAutoPanning
*
* Specifies if panning via <panGraph> should be allowed to implement autoscroll
* if no scrollbars are available in <scrollPointToVisible>. To enable panning
* inside the container, near the edge, set <mxPanningManager.border> to a
* positive value. Default is false.
*/
mxGraph.prototype.allowAutoPanning = false;
/**
* Variable: autoExtend
*
* Specifies if the size of the graph should be automatically extended if the
* mouse goes near the container edge while dragging. This is only taken into
* account if the container has scrollbars. Default is true. See <autoScroll>.
*/
mxGraph.prototype.autoExtend = true;
/**
* Variable: maximumGraphBounds
*
* <mxRectangle> that specifies the area in which all cells in the diagram
* should be placed. Uses in <getMaximumGraphBounds>. Use a width or height of
* 0 if you only want to give a upper, left corner.
*/
mxGraph.prototype.maximumGraphBounds = null;
/**
* Variable: minimumGraphSize
*
* <mxRectangle> that specifies the minimum size of the graph. This is ignored
* if the graph container has no scrollbars. Default is null.
*/
mxGraph.prototype.minimumGraphSize = null;
/**
* Variable: minimumContainerSize
*
* <mxRectangle> that specifies the minimum size of the <container> if
* <resizeContainer> is true.
*/
mxGraph.prototype.minimumContainerSize = null;
/**
* Variable: maximumContainerSize
*
* <mxRectangle> that specifies the maximum size of the container if
* <resizeContainer> is true.
*/
mxGraph.prototype.maximumContainerSize = null;
/**
* Variable: resizeContainer
*
* Specifies if the container should be resized to the graph size when
* the graph size has changed. Default is false.
*/
mxGraph.prototype.resizeContainer = false;
/**
* Variable: border
*
* Border to be added to the bottom and right side when the container is
* being resized after the graph has been changed. Default is 0.
*/
mxGraph.prototype.border = 0;
/**
* Variable: keepEdgesInForeground
*
* Specifies if edges should appear in the foreground regardless of their order
* in the model. If <keepEdgesInForeground> and <keepEdgesInBackground> are
* both true then the normal order is applied. Default is false.
*/
mxGraph.prototype.keepEdgesInForeground = false;
/**
* Variable: keepEdgesInBackground
*
* Specifies if edges should appear in the background regardless of their order
* in the model. If <keepEdgesInForeground> and <keepEdgesInBackground> are
* both true then the normal order is applied. Default is false.
*/
mxGraph.prototype.keepEdgesInBackground = false;
/**
* Variable: allowNegativeCoordinates
*
* Specifies if negative coordinates for vertices are allowed. Default is true.
*/
mxGraph.prototype.allowNegativeCoordinates = true;
/**
* Variable: constrainChildren
*
* Specifies if a child should be constrained inside the parent bounds after a
* move or resize of the child. Default is true.
*/
mxGraph.prototype.constrainChildren = true;
/**
* Variable: constrainRelativeChildren
*
* Specifies if child cells with relative geometries should be constrained
* inside the parent bounds, if <constrainChildren> is true, and/or the
* <maximumGraphBounds>. Default is false.
*/
mxGraph.prototype.constrainRelativeChildren = false;
/**
* Variable: extendParents
*
* Specifies if a parent should contain the child bounds after a resize of
* the child. Default is true. This has precedence over <constrainChildren>.
*/
mxGraph.prototype.extendParents = true;
/**
* Variable: extendParentsOnAdd
*
* Specifies if parents should be extended according to the <extendParents>
* switch if cells are added. Default is true.
*/
mxGraph.prototype.extendParentsOnAdd = true;
/**
* Variable: extendParentsOnAdd
*
* Specifies if parents should be extended according to the <extendParents>
* switch if cells are added. Default is false for backwards compatiblity.
*/
mxGraph.prototype.extendParentsOnMove = false;
/**
* Variable: recursiveResize
*
* Specifies the return value for <isRecursiveResize>. Default is
* false for backwards compatiblity.
*/
mxGraph.prototype.recursiveResize = false;
/**
* Variable: collapseToPreferredSize
*
* Specifies if the cell size should be changed to the preferred size when
* a cell is first collapsed. Default is true.
*/
mxGraph.prototype.collapseToPreferredSize = true;
/**
* Variable: zoomFactor
*
* Specifies the factor used for <zoomIn> and <zoomOut>. Default is 1.2
* (120%).
*/
mxGraph.prototype.zoomFactor = 1.2;
/**
* Variable: keepSelectionVisibleOnZoom
*
* Specifies if the viewport should automatically contain the selection cells
* after a zoom operation. Default is false.
*/
mxGraph.prototype.keepSelectionVisibleOnZoom = false;
/**
* Variable: centerZoom
*
* Specifies if the zoom operations should go into the center of the actual
* diagram rather than going from top, left. Default is true.
*/
mxGraph.prototype.centerZoom = true;
/**
* Variable: resetViewOnRootChange
*
* Specifies if the scale and translate should be reset if the root changes in
* the model. Default is true.
*/
mxGraph.prototype.resetViewOnRootChange = true;
/**
* Variable: resetEdgesOnResize
*
* Specifies if edge control points should be reset after the resize of a
* connected cell. Default is false.
*/
mxGraph.prototype.resetEdgesOnResize = false;
/**
* Variable: resetEdgesOnMove
*
* Specifies if edge control points should be reset after the move of a
* connected cell. Default is false.
*/
mxGraph.prototype.resetEdgesOnMove = false;
/**
* Variable: resetEdgesOnConnect
*
* Specifies if edge control points should be reset after the the edge has been
* reconnected. Default is true.
*/
mxGraph.prototype.resetEdgesOnConnect = true;
/**
* Variable: allowLoops
*
* Specifies if loops (aka self-references) are allowed. Default is false.
*/
mxGraph.prototype.allowLoops = false;
/**
* Variable: defaultLoopStyle
*
* <mxEdgeStyle> to be used for loops. This is a fallback for loops if the
* <mxConstants.STYLE_LOOP> is undefined. Default is <mxEdgeStyle.Loop>.
*/
mxGraph.prototype.defaultLoopStyle = mxEdgeStyle.Loop;
/**
* Variable: multigraph
*
* Specifies if multiple edges in the same direction between the same pair of
* vertices are allowed. Default is true.
*/
mxGraph.prototype.multigraph = true;
/**
* Variable: connectableEdges
*
* Specifies if edges are connectable. Default is false. This overrides the
* connectable field in edges.
*/
mxGraph.prototype.connectableEdges = false;
/**
* Variable: allowDanglingEdges
*
* Specifies if edges with disconnected terminals are allowed in the graph.
* Default is true.
*/
mxGraph.prototype.allowDanglingEdges = true;
/**
* Variable: cloneInvalidEdges
*
* Specifies if edges that are cloned should be validated and only inserted
* if they are valid. Default is true.
*/
mxGraph.prototype.cloneInvalidEdges = false;
/**
* Variable: disconnectOnMove
*
* Specifies if edges should be disconnected from their terminals when they
* are moved. Default is true.
*/
mxGraph.prototype.disconnectOnMove = true;
/**
* Variable: labelsVisible
*
* Specifies if labels should be visible. This is used in <getLabel>. Default
* is true.
*/
mxGraph.prototype.labelsVisible = true;
/**
* Variable: htmlLabels
*
* Specifies the return value for <isHtmlLabel>. Default is false.
*/
mxGraph.prototype.htmlLabels = false;
/**
* Variable: swimlaneSelectionEnabled
*
* Specifies if swimlanes should be selectable via the content if the
* mouse is released. Default is true.
*/
mxGraph.prototype.swimlaneSelectionEnabled = true;
/**
* Variable: swimlaneNesting
*
* Specifies if nesting of swimlanes is allowed. Default is true.
*/
mxGraph.prototype.swimlaneNesting = true;
/**
* Variable: swimlaneIndicatorColorAttribute
*
* The attribute used to find the color for the indicator if the indicator
* color is set to 'swimlane'. Default is <mxConstants.STYLE_FILLCOLOR>.
*/
mxGraph.prototype.swimlaneIndicatorColorAttribute = mxConstants.STYLE_FILLCOLOR;
/**
* Variable: imageBundles
*
* Holds the list of image bundles.
*/
mxGraph.prototype.imageBundles = null;
/**
* Variable: minFitScale
*
* Specifies the minimum scale to be applied in <fit>. Default is 0.1. Set this
* to null to allow any value.
*/
mxGraph.prototype.minFitScale = 0.1;
/**
* Variable: maxFitScale
*
* Specifies the maximum scale to be applied in <fit>. Default is 8. Set this
* to null to allow any value.
*/
mxGraph.prototype.maxFitScale = 8;
/**
* Variable: panDx
*
* Current horizontal panning value. Default is 0.
*/
mxGraph.prototype.panDx = 0;
/**
* Variable: panDy
*
* Current vertical panning value. Default is 0.
*/
mxGraph.prototype.panDy = 0;
/**
* Variable: collapsedImage
*
* Specifies the <mxImage> to indicate a collapsed state.
* Default value is mxClient.imageBasePath + '/collapsed.gif'
*/
mxGraph.prototype.collapsedImage = new mxImage(mxClient.imageBasePath + '/collapsed.gif', 9, 9);
/**
* Variable: expandedImage
*
* Specifies the <mxImage> to indicate a expanded state.
* Default value is mxClient.imageBasePath + '/expanded.gif'
*/
mxGraph.prototype.expandedImage = new mxImage(mxClient.imageBasePath + '/expanded.gif', 9, 9);
/**
* Variable: warningImage
*
* Specifies the <mxImage> for the image to be used to display a warning
* overlay. See <setCellWarning>. Default value is mxClient.imageBasePath +
* '/warning'. The extension for the image depends on the platform. It is
* '.png' on the Mac and '.gif' on all other platforms.
*/
mxGraph.prototype.warningImage = new mxImage(mxClient.imageBasePath + '/warning'+
((mxClient.IS_MAC) ? '.png' : '.gif'), 16, 16);
/**
* Variable: alreadyConnectedResource
*
* Specifies the resource key for the error message to be displayed in
* non-multigraphs when two vertices are already connected. If the resource
* for this key does not exist then the value is used as the error message.
* Default is 'alreadyConnected'.
*/
mxGraph.prototype.alreadyConnectedResource = (mxClient.language != 'none') ? 'alreadyConnected' : '';
/**
* Variable: containsValidationErrorsResource
*
* Specifies the resource key for the warning message to be displayed when
* a collapsed cell contains validation errors. If the resource for this
* key does not exist then the value is used as the warning message.
* Default is 'containsValidationErrors'.
*/
mxGraph.prototype.containsValidationErrorsResource = (mxClient.language != 'none') ? 'containsValidationErrors' : '';
/**
* Variable: collapseExpandResource
*
* Specifies the resource key for the tooltip on the collapse/expand icon.
* If the resource for this key does not exist then the value is used as
* the tooltip. Default is 'collapse-expand'.
*/
mxGraph.prototype.collapseExpandResource = (mxClient.language != 'none') ? 'collapse-expand' : '';
/**
* Function: init
*
* Initializes the <container> and creates the respective datastructures.
*
* Parameters:
*
* container - DOM node that will contain the graph display.
*/
mxGraph.prototype.init = function(container)
{
this.container = container;
// Initializes the in-place editor
this.cellEditor = this.createCellEditor();
// Initializes the container using the view
this.view.init();
// Updates the size of the container for the current graph
this.sizeDidChange();
// Hides tooltips and resets tooltip timer if mouse leaves container
mxEvent.addListener(container, 'mouseleave', mxUtils.bind(this, function()
{
if (this.tooltipHandler != null)
{
this.tooltipHandler.hide();
}
}));
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload', mxUtils.bind(this, function()
{
this.destroy();
}));
// Disable shift-click for text
mxEvent.addListener(container, 'selectstart',
mxUtils.bind(this, function(evt)
{
return this.isEditing() || (!this.isMouseDown && !mxEvent.isShiftDown(evt));
})
);
}
// Workaround for missing last shape and connect preview in IE8 standards
// mode if no initial graph displayed or no label for shape defined
if (document.documentMode == 8)
{
container.insertAdjacentHTML('beforeend', '<' + mxClient.VML_PREFIX + ':group' +
' style="DISPLAY: none;"></' + mxClient.VML_PREFIX + ':group>');
}
};
/**
* Function: createHandlers
*
* Creates the tooltip-, panning-, connection- and graph-handler (in this
* order). This is called in the constructor before <init> is called.
*/
mxGraph.prototype.createHandlers = function()
{
this.tooltipHandler = this.createTooltipHandler();
this.tooltipHandler.setEnabled(false);
this.selectionCellsHandler = this.createSelectionCellsHandler();
this.connectionHandler = this.createConnectionHandler();
this.connectionHandler.setEnabled(false);
this.graphHandler = this.createGraphHandler();
this.panningHandler = this.createPanningHandler();
this.panningHandler.panningEnabled = false;
this.popupMenuHandler = this.createPopupMenuHandler();
};
/**
* Function: createTooltipHandler
*
* Creates and returns a new <mxTooltipHandler> to be used in this graph.
*/
mxGraph.prototype.createTooltipHandler = function()
{
return new mxTooltipHandler(this);
};
/**
* Function: createSelectionCellsHandler
*
* Creates and returns a new <mxTooltipHandler> to be used in this graph.
*/
mxGraph.prototype.createSelectionCellsHandler = function()
{
return new mxSelectionCellsHandler(this);
};
/**
* Function: createConnectionHandler
*
* Creates and returns a new <mxConnectionHandler> to be used in this graph.
*/
mxGraph.prototype.createConnectionHandler = function()
{
return new mxConnectionHandler(this);
};
/**
* Function: createGraphHandler
*
* Creates and returns a new <mxGraphHandler> to be used in this graph.
*/
mxGraph.prototype.createGraphHandler = function()
{
return new mxGraphHandler(this);
};
/**
* Function: createPanningHandler
*
* Creates and returns a new <mxPanningHandler> to be used in this graph.
*/
mxGraph.prototype.createPanningHandler = function()
{
return new mxPanningHandler(this);
};
/**
* Function: createPopupMenuHandler
*
* Creates and returns a new <mxPopupMenuHandler> to be used in this graph.
*/
mxGraph.prototype.createPopupMenuHandler = function()
{
return new mxPopupMenuHandler(this);
};
/**
* Function: createSelectionModel
*
* Creates a new <mxGraphSelectionModel> to be used in this graph.
*/
mxGraph.prototype.createSelectionModel = function()
{
return new mxGraphSelectionModel(this);
};
/**
* Function: createStylesheet
*
* Creates a new <mxGraphSelectionModel> to be used in this graph.
*/
mxGraph.prototype.createStylesheet = function()
{
return new mxStylesheet();
};
/**
* Function: createGraphView
*
* Creates a new <mxGraphView> to be used in this graph.
*/
mxGraph.prototype.createGraphView = function()
{
return new mxGraphView(this);
};
/**
* Function: createCellRenderer
*
* Creates a new <mxCellRenderer> to be used in this graph.
*/
mxGraph.prototype.createCellRenderer = function()
{
return new mxCellRenderer();
};
/**
* Function: createCellEditor
*
* Creates a new <mxCellEditor> to be used in this graph.
*/
mxGraph.prototype.createCellEditor = function()
{
return new mxCellEditor(this);
};
/**
* Function: getModel
*
* Returns the <mxGraphModel> that contains the cells.
*/
mxGraph.prototype.getModel = function()
{
return this.model;
};
/**
* Function: getView
*
* Returns the <mxGraphView> that contains the <mxCellStates>.
*/
mxGraph.prototype.getView = function()
{
return this.view;
};
/**
* Function: getStylesheet
*
* Returns the <mxStylesheet> that defines the style.
*/
mxGraph.prototype.getStylesheet = function()
{
return this.stylesheet;
};
/**
* Function: setStylesheet
*
* Sets the <mxStylesheet> that defines the style.
*/
mxGraph.prototype.setStylesheet = function(stylesheet)
{
this.stylesheet = stylesheet;
};
/**
* Function: getSelectionModel
*
* Returns the <mxGraphSelectionModel> that contains the selection.
*/
mxGraph.prototype.getSelectionModel = function()
{
return this.selectionModel;
};
/**
* Function: setSelectionModel
*
* Sets the <mxSelectionModel> that contains the selection.
*/
mxGraph.prototype.setSelectionModel = function(selectionModel)
{
this.selectionModel = selectionModel;
};
/**
* Function: getSelectionCellsForChanges
*
* Returns the cells to be selected for the given array of changes.
*/
mxGraph.prototype.getSelectionCellsForChanges = function(changes)
{
var dict = new mxDictionary();
var cells = [];
var addCell = mxUtils.bind(this, function(cell)
{
if (!dict.get(cell) && this.model.contains(cell))
{
if (this.model.isEdge(cell) || this.model.isVertex(cell))
{
dict.put(cell, true);
cells.push(cell);
}
else
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
addCell(this.model.getChildAt(cell, i));
}
}
}
});
for (var i = 0; i < changes.length; i++)
{
var change = changes[i];
if (change.constructor != mxRootChange)
{
var cell = null;
if (change instanceof mxChildChange)
{
cell = change.child;
}
else if (change.cell != null && change.cell instanceof mxCell)
{
cell = change.cell;
}
if (cell != null)
{
addCell(cell);
}
}
}
return cells;
};
/**
* Function: graphModelChanged
*
* Called when the graph model changes. Invokes <processChange> on each
* item of the given array to update the view accordingly.
*
* Parameters:
*
* changes - Array that contains the individual changes.
*/
mxGraph.prototype.graphModelChanged = function(changes)
{
for (var i = 0; i < changes.length; i++)
{
this.processChange(changes[i]);
}
this.updateSelection();
this.view.validate();
this.sizeDidChange();
};
/**
* Function: updateSelection
*
* Removes selection cells that are not in the model from the selection.
*/
mxGraph.prototype.updateSelection = function()
{
var cells = this.getSelectionCells();
var removed = [];
for (var i = 0; i < cells.length; i++)
{
if (!this.model.contains(cells[i]) || !this.isCellVisible(cells[i]))
{
removed.push(cells[i]);
}
else
{
var par = this.model.getParent(cells[i]);
while (par != null && par != this.view.currentRoot)
{
if (this.isCellCollapsed(par) || !this.isCellVisible(par))
{
removed.push(cells[i]);
break;
}
par = this.model.getParent(par);
}
}
}
this.removeSelectionCells(removed);
};
/**
* Function: processChange
*
* Processes the given change and invalidates the respective cached data
* in <view>. This fires a <root> event if the root has changed in the
* model.
*
* Parameters:
*
* change - Object that represents the change on the model.
*/
mxGraph.prototype.processChange = function(change)
{
// Resets the view settings, removes all cells and clears
// the selection if the root changes.
if (change instanceof mxRootChange)
{
this.clearSelection();
this.setDefaultParent(null);
this.removeStateForCell(change.previous);
if (this.resetViewOnRootChange)
{
this.view.scale = 1;
this.view.translate.x = 0;
this.view.translate.y = 0;
}
this.fireEvent(new mxEventObject(mxEvent.ROOT));
}
// Adds or removes a child to the view by online invaliding
// the minimal required portions of the cache, namely, the
// old and new parent and the child.
else if (change instanceof mxChildChange)
{
var newParent = this.model.getParent(change.child);
this.view.invalidate(change.child, true, true);
if (!this.model.contains(newParent) || this.isCellCollapsed(newParent))
{
this.view.invalidate(change.child, true, true);
this.removeStateForCell(change.child);
// Handles special case of current root of view being removed
if (this.view.currentRoot == change.child)
{
this.home();
}
}
if (newParent != change.previous)
{
// Refreshes the collapse/expand icons on the parents
if (newParent != null)
{
this.view.invalidate(newParent, false, false);
}
if (change.previous != null)
{
this.view.invalidate(change.previous, false, false);
}
}
}
// Handles two special cases where the shape does not need to be
// recreated from scratch, it only needs to be invalidated.
else if (change instanceof mxTerminalChange || change instanceof mxGeometryChange)
{
// Checks if the geometry has changed to avoid unnessecary revalidation
if (change instanceof mxTerminalChange || ((change.previous == null && change.geometry != null) ||
(change.previous != null && !change.previous.equals(change.geometry))))
{
this.view.invalidate(change.cell);
}
}
// Handles two special cases where only the shape, but no
// descendants need to be recreated
else if (change instanceof mxValueChange)
{
this.view.invalidate(change.cell, false, false);
}
// Requires a new mxShape in JavaScript
else if (change instanceof mxStyleChange)
{
this.view.invalidate(change.cell, true, true);
var state = this.view.getState(change.cell);
if (state != null)
{
state.invalidStyle = true;
}
}
// Removes the state from the cache by default
else if (change.cell != null && change.cell instanceof mxCell)
{
this.removeStateForCell(change.cell);
}
};
/**
* Function: removeStateForCell
*
* Removes all cached information for the given cell and its descendants.
* This is called when a cell was removed from the model.
*
* Paramters:
*
* cell - <mxCell> that was removed from the model.
*/
mxGraph.prototype.removeStateForCell = function(cell)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.removeStateForCell(this.model.getChildAt(cell, i));
}
this.view.invalidate(cell, false, true);
this.view.removeState(cell);
};
/**
* Group: Overlays
*/
/**
* Function: addCellOverlay
*
* Adds an <mxCellOverlay> for the specified cell. This method fires an
* <addoverlay> event and returns the new <mxCellOverlay>.
*
* Parameters:
*
* cell - <mxCell> to add the overlay for.
* overlay - <mxCellOverlay> to be added for the cell.
*/
mxGraph.prototype.addCellOverlay = function(cell, overlay)
{
if (cell.overlays == null)
{
cell.overlays = [];
}
cell.overlays.push(overlay);
var state = this.view.getState(cell);
// Immediately updates the cell display if the state exists
if (state != null)
{
this.cellRenderer.redraw(state);
}
this.fireEvent(new mxEventObject(mxEvent.ADD_OVERLAY,
'cell', cell, 'overlay', overlay));
return overlay;
};
/**
* Function: getCellOverlays
*
* Returns the array of <mxCellOverlays> for the given cell or null, if
* no overlays are defined.
*
* Parameters:
*
* cell - <mxCell> whose overlays should be returned.
*/
mxGraph.prototype.getCellOverlays = function(cell)
{
return cell.overlays;
};
/**
* Function: removeCellOverlay
*
* Removes and returns the given <mxCellOverlay> from the given cell. This
* method fires a <removeoverlay> event. If no overlay is given, then all
* overlays are removed using <removeOverlays>.
*
* Parameters:
*
* cell - <mxCell> whose overlay should be removed.
* overlay - Optional <mxCellOverlay> to be removed.
*/
mxGraph.prototype.removeCellOverlay = function(cell, overlay)
{
if (overlay == null)
{
this.removeCellOverlays(cell);
}
else
{
var index = mxUtils.indexOf(cell.overlays, overlay);
if (index >= 0)
{
cell.overlays.splice(index, 1);
if (cell.overlays.length == 0)
{
cell.overlays = null;
}
// Immediately updates the cell display if the state exists
var state = this.view.getState(cell);
if (state != null)
{
this.cellRenderer.redraw(state);
}
this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY,
'cell', cell, 'overlay', overlay));
}
else
{
overlay = null;
}
}
return overlay;
};
/**
* Function: removeCellOverlays
*
* Removes all <mxCellOverlays> from the given cell. This method
* fires a <removeoverlay> event for each <mxCellOverlay> and returns
* the array of <mxCellOverlays> that was removed from the cell.
*
* Parameters:
*
* cell - <mxCell> whose overlays should be removed
*/
mxGraph.prototype.removeCellOverlays = function(cell)
{
var overlays = cell.overlays;
if (overlays != null)
{
cell.overlays = null;
// Immediately updates the cell display if the state exists
var state = this.view.getState(cell);
if (state != null)
{
this.cellRenderer.redraw(state);
}
for (var i = 0; i < overlays.length; i++)
{
this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY,
'cell', cell, 'overlay', overlays[i]));
}
}
return overlays;
};
/**
* Function: clearCellOverlays
*
* Removes all <mxCellOverlays> in the graph for the given cell and all its
* descendants. If no cell is specified then all overlays are removed from
* the graph. This implementation uses <removeCellOverlays> to remove the
* overlays from the individual cells.
*
* Parameters:
*
* cell - Optional <mxCell> that represents the root of the subtree to
* remove the overlays from. Default is the root in the model.
*/
mxGraph.prototype.clearCellOverlays = function(cell)
{
cell = (cell != null) ? cell : this.model.getRoot();
this.removeCellOverlays(cell);
// Recursively removes all overlays from the children
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = this.model.getChildAt(cell, i);
this.clearCellOverlays(child); // recurse
}
};
/**
* Function: setCellWarning
*
* Creates an overlay for the given cell using the warning and image or
* <warningImage> and returns the new <mxCellOverlay>. The warning is
* displayed as a tooltip in a red font and may contain HTML markup. If
* the warning is null or a zero length string, then all overlays are
* removed from the cell.
*
* Example:
*
* (code)
* graph.setCellWarning(cell, '<b>Warning:</b>: Hello, World!');
* (end)
*
* Parameters:
*
* cell - <mxCell> whose warning should be set.
* warning - String that represents the warning to be displayed.
* img - Optional <mxImage> to be used for the overlay. Default is
* <warningImage>.
* isSelect - Optional boolean indicating if a click on the overlay
* should select the corresponding cell. Default is false.
*/
mxGraph.prototype.setCellWarning = function(cell, warning, img, isSelect)
{
if (warning != null && warning.length > 0)
{
img = (img != null) ? img : this.warningImage;
// Creates the overlay with the image and warning
var overlay = new mxCellOverlay(img,
'<font color=red>'+warning+'</font>');
// Adds a handler for single mouseclicks to select the cell
if (isSelect)
{
overlay.addListener(mxEvent.CLICK,
mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.setSelectionCell(cell);
}
})
);
}
// Sets and returns the overlay in the graph
return this.addCellOverlay(cell, overlay);
}
else
{
this.removeCellOverlays(cell);
}
return null;
};
/**
* Group: In-place editing
*/
/**
* Function: startEditing
*
* Calls <startEditingAtCell> using the given cell or the first selection
* cell.
*
* Parameters:
*
* evt - Optional mouse event that triggered the editing.
*/
mxGraph.prototype.startEditing = function(evt)
{
this.startEditingAtCell(null, evt);
};
/**
* Function: startEditingAtCell
*
* Fires a <startEditing> event and invokes <mxCellEditor.startEditing>
* on <editor>. After editing was started, a <editingStarted> event is
* fired.
*
* Parameters:
*
* cell - <mxCell> to start the in-place editor for.
* evt - Optional mouse event that triggered the editing.
*/
mxGraph.prototype.startEditingAtCell = function(cell, evt)
{
if (evt == null || !mxEvent.isMultiTouchEvent(evt))
{
if (cell == null)
{
cell = this.getSelectionCell();
if (cell != null && !this.isCellEditable(cell))
{
cell = null;
}
}
if (cell != null)
{
this.fireEvent(new mxEventObject(mxEvent.START_EDITING,
'cell', cell, 'event', evt));
this.cellEditor.startEditing(cell, evt);
this.fireEvent(new mxEventObject(mxEvent.EDITING_STARTED,
'cell', cell, 'event', evt));
}
}
};
/**
* Function: getEditingValue
*
* Returns the initial value for in-place editing. This implementation
* returns <convertValueToString> for the given cell. If this function is
* overridden, then <mxGraphModel.valueForCellChanged> should take care
* of correctly storing the actual new value inside the user object.
*
* Parameters:
*
* cell - <mxCell> for which the initial editing value should be returned.
* evt - Optional mouse event that triggered the editor.
*/
mxGraph.prototype.getEditingValue = function(cell, evt)
{
return this.convertValueToString(cell);
};
/**
* Function: stopEditing
*
* Stops the current editing and fires a <editingStopped> event.
*
* Parameters:
*
* cancel - Boolean that specifies if the current editing value
* should be stored.
*/
mxGraph.prototype.stopEditing = function(cancel)
{
this.cellEditor.stopEditing(cancel);
this.fireEvent(new mxEventObject(mxEvent.EDITING_STOPPED, 'cancel', cancel));
};
/**
* Function: labelChanged
*
* Sets the label of the specified cell to the given value using
* <cellLabelChanged> and fires <mxEvent.LABEL_CHANGED> while the
* transaction is in progress. Returns the cell whose label was changed.
*
* Parameters:
*
* cell - <mxCell> whose label should be changed.
* value - New label to be assigned.
* evt - Optional event that triggered the change.
*/
mxGraph.prototype.labelChanged = function(cell, value, evt)
{
this.model.beginUpdate();
try
{
var old = cell.value;
this.cellLabelChanged(cell, value, this.isAutoSizeCell(cell));
this.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED,
'cell', cell, 'value', value, 'old', old, 'event', evt));
}
finally
{
this.model.endUpdate();
}
return cell;
};
/**
* Function: cellLabelChanged
*
* Sets the new label for a cell. If autoSize is true then
* <cellSizeUpdated> will be called.
*
* In the following example, the function is extended to map changes to
* attributes in an XML node, as shown in <convertValueToString>.
* Alternatively, the handling of this can be implemented as shown in
* <mxGraphModel.valueForCellChanged> without the need to clone the
* user object.
*
* (code)
* var graphCellLabelChanged = graph.cellLabelChanged;
* graph.cellLabelChanged = function(cell, newValue, autoSize)
* {
* // Cloned for correct undo/redo
* var elt = cell.value.cloneNode(true);
* elt.setAttribute('label', newValue);
*
* newValue = elt;
* graphCellLabelChanged.apply(this, arguments);
* };
* (end)
*
* Parameters:
*
* cell - <mxCell> whose label should be changed.
* value - New label to be assigned.
* autoSize - Boolean that specifies if <cellSizeUpdated> should be called.
*/
mxGraph.prototype.cellLabelChanged = function(cell, value, autoSize)
{
this.model.beginUpdate();
try
{
this.model.setValue(cell, value);
if (autoSize)
{
this.cellSizeUpdated(cell, false);
}
}
finally
{
this.model.endUpdate();
}
};
/**
* Group: Event processing
*/
/**
* Function: escape
*
* Processes an escape keystroke.
*
* Parameters:
*
* evt - Mouseevent that represents the keystroke.
*/
mxGraph.prototype.escape = function(evt)
{
this.fireEvent(new mxEventObject(mxEvent.ESCAPE, 'event', evt));
};
/**
* Function: click
*
* Processes a singleclick on an optional cell and fires a <click> event.
* The click event is fired initially. If the graph is enabled and the
* event has not been consumed, then the cell is selected using
* <selectCellForEvent> or the selection is cleared using
* <clearSelection>. The events consumed state is set to true if the
* corresponding <mxMouseEvent> has been consumed.
*
* To handle a click event, use the following code.
*
* (code)
* graph.addListener(mxEvent.CLICK, function(sender, evt)
* {
* var e = evt.getProperty('event'); // mouse event
* var cell = evt.getProperty('cell'); // cell may be null
*
* if (cell != null)
* {
* // Do something useful with cell and consume the event
* evt.consume();
* }
* });
* (end)
*
* Parameters:
*
* me - <mxMouseEvent> that represents the single click.
*/
mxGraph.prototype.click = function(me)
{
var evt = me.getEvent();
var cell = me.getCell();
var mxe = new mxEventObject(mxEvent.CLICK, 'event', evt, 'cell', cell);
if (me.isConsumed())
{
mxe.consume();
}
this.fireEvent(mxe);
// Handles the event if it has not been consumed
if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed())
{
if (cell != null)
{
if (this.isTransparentClickEvent(evt))
{
var active = false;
var tmp = this.getCellAt(me.graphX, me.graphY, null, null, null, mxUtils.bind(this, function(state)
{
var selected = this.isCellSelected(state.cell);
active = active || selected;
return !active || selected;
}));
if (tmp != null)
{
cell = tmp;
}
}
this.selectCellForEvent(cell, evt);
}
else
{
var swimlane = null;
if (this.isSwimlaneSelectionEnabled())
{
// Gets the swimlane at the location (includes
// content area of swimlanes)
swimlane = this.getSwimlaneAt(me.getGraphX(), me.getGraphY());
}
// Selects the swimlane and consumes the event
if (swimlane != null)
{
this.selectCellForEvent(swimlane, evt);
}
// Ignores the event if the control key is pressed
else if (!this.isToggleEvent(evt))
{
this.clearSelection();
}
}
}
};
/**
* Function: dblClick
*
* Processes a doubleclick on an optional cell and fires a <dblclick>
* event. The event is fired initially. If the graph is enabled and the
* event has not been consumed, then <edit> is called with the given
* cell. The event is ignored if no cell was specified.
*
* Example for overriding this method.
*
* (code)
* graph.dblClick = function(evt, cell)
* {
* var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell);
* this.fireEvent(mxe);
*
* if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed())
* {
* mxUtils.alert('Hello, World!');
* mxe.consume();
* }
* }
* (end)
*
* Example listener for this event.
*
* (code)
* graph.addListener(mxEvent.DOUBLE_CLICK, function(sender, evt)
* {
* var cell = evt.getProperty('cell');
* // do something with the cell and consume the
* // event to prevent in-place editing from start
* });
* (end)
*
* Parameters:
*
* evt - Mouseevent that represents the doubleclick.
* cell - Optional <mxCell> under the mousepointer.
*/
mxGraph.prototype.dblClick = function(evt, cell)
{
var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell);
this.fireEvent(mxe);
// Handles the event if it has not been consumed
if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed() &&
cell != null && this.isCellEditable(cell) && !this.isEditing(cell))
{
this.startEditingAtCell(cell, evt);
mxEvent.consume(evt);
}
};
/**
* Function: tapAndHold
*
* Handles the <mxMouseEvent> by highlighting the <mxCellState>.
*
* Parameters:
*
* me - <mxMouseEvent> that represents the touch event.
* state - Optional <mxCellState> that is associated with the event.
*/
mxGraph.prototype.tapAndHold = function(me)
{
var evt = me.getEvent();
var mxe = new mxEventObject(mxEvent.TAP_AND_HOLD, 'event', evt, 'cell', me.getCell());
// LATER: Check if event should be consumed if me is consumed
this.fireEvent(mxe);
if (mxe.isConsumed())
{
// Resets the state of the panning handler
this.panningHandler.panningTrigger = false;
}
// Handles the event if it has not been consumed
if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed() && this.connectionHandler.isEnabled())
{
var state = this.view.getState(this.connectionHandler.marker.getCell(me));
if (state != null)
{
this.connectionHandler.marker.currentColor = this.connectionHandler.marker.validColor;
this.connectionHandler.marker.markedState = state;
this.connectionHandler.marker.mark();
this.connectionHandler.first = new mxPoint(me.getGraphX(), me.getGraphY());
this.connectionHandler.edgeState = this.connectionHandler.createEdgeState(me);
this.connectionHandler.previous = state;
this.connectionHandler.fireEvent(new mxEventObject(mxEvent.START, 'state', this.connectionHandler.previous));
}
}
};
/**
* Function: scrollPointToVisible
*
* Scrolls the graph to the given point, extending the graph container if
* specified.
*/
mxGraph.prototype.scrollPointToVisible = function(x, y, extend, border)
{
if (!this.timerAutoScroll && (this.ignoreScrollbars || mxUtils.hasScrollbars(this.container)))
{
var c = this.container;
border = (border != null) ? border : 20;
if (x >= c.scrollLeft && y >= c.scrollTop && x <= c.scrollLeft + c.clientWidth &&
y <= c.scrollTop + c.clientHeight)
{
var dx = c.scrollLeft + c.clientWidth - x;
if (dx < border)
{
var old = c.scrollLeft;
c.scrollLeft += border - dx;
// Automatically extends the canvas size to the bottom, right
// if the event is outside of the canvas and the edge of the
// canvas has been reached. Notes: Needs fix for IE.
if (extend && old == c.scrollLeft)
{
if (this.dialect == mxConstants.DIALECT_SVG)
{
var root = this.view.getDrawPane().ownerSVGElement;
var width = this.container.scrollWidth + border - dx;
// Updates the clipping region. This is an expensive
// operation that should not be executed too often.
root.style.width = width + 'px';
}
else
{
var width = Math.max(c.clientWidth, c.scrollWidth) + border - dx;
var canvas = this.view.getCanvas();
canvas.style.width = width + 'px';
}
c.scrollLeft += border - dx;
}
}
else
{
dx = x - c.scrollLeft;
if (dx < border)
{
c.scrollLeft -= border - dx;
}
}
var dy = c.scrollTop + c.clientHeight - y;
if (dy < border)
{
var old = c.scrollTop;
c.scrollTop += border - dy;
if (old == c.scrollTop && extend)
{
if (this.dialect == mxConstants.DIALECT_SVG)
{
var root = this.view.getDrawPane().ownerSVGElement;
var height = this.container.scrollHeight + border - dy;
// Updates the clipping region. This is an expensive
// operation that should not be executed too often.
root.style.height = height + 'px';
}
else
{
var height = Math.max(c.clientHeight, c.scrollHeight) + border - dy;
var canvas = this.view.getCanvas();
canvas.style.height = height + 'px';
}
c.scrollTop += border - dy;
}
}
else
{
dy = y - c.scrollTop;
if (dy < border)
{
c.scrollTop -= border - dy;
}
}
}
}
else if (this.allowAutoPanning && !this.panningHandler.isActive())
{
if (this.panningManager == null)
{
this.panningManager = this.createPanningManager();
}
this.panningManager.panTo(x + this.panDx, y + this.panDy);
}
};
/**
* Function: createPanningManager
*
* Creates and returns an <mxPanningManager>.
*/
mxGraph.prototype.createPanningManager = function()
{
return new mxPanningManager(this);
};
/**
* Function: getBorderSizes
*
* Returns the size of the border and padding on all four sides of the
* container. The left, top, right and bottom borders are stored in the x, y,
* width and height of the returned <mxRectangle>, respectively.
*/
mxGraph.prototype.getBorderSizes = function()
{
var css = mxUtils.getCurrentStyle(this.container);
return new mxRectangle(mxUtils.parseCssNumber(css.paddingLeft) +
((css.borderLeftStyle != 'none') ? mxUtils.parseCssNumber(css.borderLeftWidth) : 0),
mxUtils.parseCssNumber(css.paddingTop) +
((css.borderTopStyle != 'none') ? mxUtils.parseCssNumber(css.borderTopWidth) : 0),
mxUtils.parseCssNumber(css.paddingRight) +
((css.borderRightStyle != 'none') ? mxUtils.parseCssNumber(css.borderRightWidth) : 0),
mxUtils.parseCssNumber(css.paddingBottom) +
((css.borderBottomStyle != 'none') ? mxUtils.parseCssNumber(css.borderBottomWidth) : 0));
};
/**
* Function: getPreferredPageSize
*
* Returns the preferred size of the background page if <preferPageSize> is true.
*/
mxGraph.prototype.getPreferredPageSize = function(bounds, width, height)
{
var scale = this.view.scale;
var tr = this.view.translate;
var fmt = this.pageFormat;
var ps = this.pageScale;
var page = new mxRectangle(0, 0, Math.ceil(fmt.width * ps), Math.ceil(fmt.height * ps));
var hCount = (this.pageBreaksVisible) ? Math.ceil(width / page.width) : 1;
var vCount = (this.pageBreaksVisible) ? Math.ceil(height / page.height) : 1;
return new mxRectangle(0, 0, hCount * page.width + 2 + tr.x, vCount * page.height + 2 + tr.y);
};
/**
* Function: fit
*
* Scales the graph such that the complete diagram fits into <container> and
* returns the current scale in the view. To fit an initial graph prior to
* rendering, set <mxGraphView.rendering> to false prior to changing the model
* and execute the following after changing the model.
*
* (code)
* graph.fit();
* graph.view.rendering = true;
* graph.refresh();
* (end)
*
* To fit and center the graph, the following code can be used.
*
* (code)
* var margin = 2;
* var max = 3;
*
* var bounds = graph.getGraphBounds();
* var cw = graph.container.clientWidth - margin;
* var ch = graph.container.clientHeight - margin;
* var w = bounds.width / graph.view.scale;
* var h = bounds.height / graph.view.scale;
* var s = Math.min(max, Math.min(cw / w, ch / h));
*
* graph.view.scaleAndTranslate(s,
* (margin + cw - w * s) / (2 * s) - bounds.x / graph.view.scale,
* (margin + ch - h * s) / (2 * s) - bounds.y / graph.view.scale);
* (end)
*
* Parameters:
*
* border - Optional number that specifies the border. Default is <border>.
* keepOrigin - Optional boolean that specifies if the translate should be
* changed. Default is false.
* margin - Optional margin in pixels. Default is 0.
* enabled - Optional boolean that specifies if the scale should be set or
* just returned. Default is true.
* ignoreWidth - Optional boolean that specifies if the width should be
* ignored. Default is false.
* ignoreHeight - Optional boolean that specifies if the height should be
* ignored. Default is false.
* maxHeight - Optional maximum height.
*/
mxGraph.prototype.fit = function(border, keepOrigin, margin, enabled, ignoreWidth, ignoreHeight, maxHeight)
{
if (this.container != null)
{
border = (border != null) ? border : this.getBorder();
keepOrigin = (keepOrigin != null) ? keepOrigin : false;
margin = (margin != null) ? margin : 0;
enabled = (enabled != null) ? enabled : true;
ignoreWidth = (ignoreWidth != null) ? ignoreWidth : false;
ignoreHeight = (ignoreHeight != null) ? ignoreHeight : false;
// Adds spacing and border from css
var cssBorder = this.getBorderSizes();
var w1 = this.container.offsetWidth - cssBorder.x - cssBorder.width - 1;
var h1 = (maxHeight != null) ? maxHeight : this.container.offsetHeight - cssBorder.y - cssBorder.height - 1;
var bounds = this.view.getGraphBounds();
if (bounds.width > 0 && bounds.height > 0)
{
if (keepOrigin && bounds.x != null && bounds.y != null)
{
bounds = bounds.clone();
bounds.width += bounds.x;
bounds.height += bounds.y;
bounds.x = 0;
bounds.y = 0;
}
// LATER: Use unscaled bounding boxes to fix rounding errors
var s = this.view.scale;
var w2 = bounds.width / s;
var h2 = bounds.height / s;
// Fits to the size of the background image if required
if (this.backgroundImage != null)
{
w2 = Math.max(w2, this.backgroundImage.width - bounds.x / s);
h2 = Math.max(h2, this.backgroundImage.height - bounds.y / s);
}
var b = ((keepOrigin) ? border : 2 * border) + margin + 1;
w1 -= b;
h1 -= b;
var s2 = (((ignoreWidth) ? h1 / h2 : (ignoreHeight) ? w1 / w2 :
Math.min(w1 / w2, h1 / h2)));
if (this.minFitScale != null)
{
s2 = Math.max(s2, this.minFitScale);
}
if (this.maxFitScale != null)
{
s2 = Math.min(s2, this.maxFitScale);
}
if (enabled)
{
if (!keepOrigin)
{
if (!mxUtils.hasScrollbars(this.container))
{
var x0 = (bounds.x != null) ? Math.floor(this.view.translate.x - bounds.x / s + border / s2 + margin / 2) : border;
var y0 = (bounds.y != null) ? Math.floor(this.view.translate.y - bounds.y / s + border / s2 + margin / 2) : border;
this.view.scaleAndTranslate(s2, x0, y0);
}
else
{
this.view.setScale(s2);
var b2 = this.getGraphBounds();
if (b2.x != null)
{
this.container.scrollLeft = b2.x;
}
if (b2.y != null)
{
this.container.scrollTop = b2.y;
}
}
}
else if (this.view.scale != s2)
{
this.view.setScale(s2);
}
}
else
{
return s2;
}
}
}
return this.view.scale;
};
/**
* Function: sizeDidChange
*
* Called when the size of the graph has changed. This implementation fires
* a <size> event after updating the clipping region of the SVG element in
* SVG-bases browsers.
*/
mxGraph.prototype.sizeDidChange = function()
{
var bounds = this.getGraphBounds();
if (this.container != null)
{
var border = this.getBorder();
var width = Math.max(0, bounds.x + bounds.width + border);
var height = Math.max(0, bounds.y + bounds.height + border);
if (this.minimumContainerSize != null)
{
width = Math.max(width, this.minimumContainerSize.width);
height = Math.max(height, this.minimumContainerSize.height);
}
if (this.resizeContainer)
{
this.doResizeContainer(width, height);
}
if (this.preferPageSize || (!mxClient.IS_IE && this.pageVisible))
{
var size = this.getPreferredPageSize(bounds, Math.max(1, width), Math.max(1, height));
if (size != null)
{
width = size.width * this.view.scale;
height = size.height * this.view.scale;
}
}
if (this.minimumGraphSize != null)
{
width = Math.max(width, this.minimumGraphSize.width * this.view.scale);
height = Math.max(height, this.minimumGraphSize.height * this.view.scale);
}
width = Math.ceil(width);
height = Math.ceil(height);
if (this.dialect == mxConstants.DIALECT_SVG)
{
var root = this.view.getDrawPane().ownerSVGElement;
if (root != null)
{
root.style.minWidth = Math.max(1, width) + 'px';
root.style.minHeight = Math.max(1, height) + 'px';
root.style.width = '100%';
root.style.height = '100%';
}
}
else
{
if (mxClient.IS_QUIRKS)
{
// Quirks mode does not support minWidth/-Height
this.view.updateHtmlCanvasSize(Math.max(1, width), Math.max(1, height));
}
else
{
this.view.canvas.style.minWidth = Math.max(1, width) + 'px';
this.view.canvas.style.minHeight = Math.max(1, height) + 'px';
}
}
this.updatePageBreaks(this.pageBreaksVisible, width, height);
}
this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', bounds));
};
/**
* Function: doResizeContainer
*
* Resizes the container for the given graph width and height.
*/
mxGraph.prototype.doResizeContainer = function(width, height)
{
if (this.maximumContainerSize != null)
{
width = Math.min(this.maximumContainerSize.width, width);
height = Math.min(this.maximumContainerSize.height, height);
}
this.container.style.width = Math.ceil(width) + 'px';
this.container.style.height = Math.ceil(height) + 'px';
};
/**
* Function: updatePageBreaks
*
* Invokes from <sizeDidChange> to redraw the page breaks.
*
* Parameters:
*
* visible - Boolean that specifies if page breaks should be shown.
* width - Specifies the width of the container in pixels.
* height - Specifies the height of the container in pixels.
*/
mxGraph.prototype.updatePageBreaks = function(visible, width, height)
{
var scale = this.view.scale;
var tr = this.view.translate;
var fmt = this.pageFormat;
var ps = scale * this.pageScale;
var bounds = new mxRectangle(0, 0, fmt.width * ps, fmt.height * ps);
var gb = mxRectangle.fromRectangle(this.getGraphBounds());
gb.width = Math.max(1, gb.width);
gb.height = Math.max(1, gb.height);
bounds.x = Math.floor((gb.x - tr.x * scale) / bounds.width) * bounds.width + tr.x * scale;
bounds.y = Math.floor((gb.y - tr.y * scale) / bounds.height) * bounds.height + tr.y * scale;
gb.width = Math.ceil((gb.width + (gb.x - bounds.x)) / bounds.width) * bounds.width;
gb.height = Math.ceil((gb.height + (gb.y - bounds.y)) / bounds.height) * bounds.height;
// Does not show page breaks if the scale is too small
visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist;
var horizontalCount = (visible) ? Math.ceil(gb.height / bounds.height) + 1 : 0;
var verticalCount = (visible) ? Math.ceil(gb.width / bounds.width) + 1 : 0;
var right = (verticalCount - 1) * bounds.width;
var bottom = (horizontalCount - 1) * bounds.height;
if (this.horizontalPageBreaks == null && horizontalCount > 0)
{
this.horizontalPageBreaks = [];
}
if (this.verticalPageBreaks == null && verticalCount > 0)
{
this.verticalPageBreaks = [];
}
var drawPageBreaks = mxUtils.bind(this, function(breaks)
{
if (breaks != null)
{
var count = (breaks == this.horizontalPageBreaks) ? horizontalCount : verticalCount;
for (var i = 0; i <= count; i++)
{
var pts = (breaks == this.horizontalPageBreaks) ?
[new mxPoint(Math.round(bounds.x), Math.round(bounds.y + i * bounds.height)),
new mxPoint(Math.round(bounds.x + right), Math.round(bounds.y + i * bounds.height))] :
[new mxPoint(Math.round(bounds.x + i * bounds.width), Math.round(bounds.y)),
new mxPoint(Math.round(bounds.x + i * bounds.width), Math.round(bounds.y + bottom))];
if (breaks[i] != null)
{
breaks[i].points = pts;
breaks[i].redraw();
}
else
{
var pageBreak = new mxPolyline(pts, this.pageBreakColor);
pageBreak.dialect = this.dialect;
pageBreak.pointerEvents = false;
pageBreak.isDashed = this.pageBreakDashed;
pageBreak.init(this.view.backgroundPane);
pageBreak.redraw();
breaks[i] = pageBreak;
}
}
for (var i = count; i < breaks.length; i++)
{
breaks[i].destroy();
}
breaks.splice(count, breaks.length - count);
}
});
drawPageBreaks(this.horizontalPageBreaks);
drawPageBreaks(this.verticalPageBreaks);
};
/**
* Group: Cell styles
*/
/**
* Function: getCellStyle
*
* Returns an array of key, value pairs representing the cell style for the
* given cell. If no string is defined in the model that specifies the
* style, then the default style for the cell is returned or an empty object,
* if no style can be found. Note: You should try and get the cell state
* for the given cell and use the cached style in the state before using
* this method.
*
* Parameters:
*
* cell - <mxCell> whose style should be returned as an array.
*/
mxGraph.prototype.getCellStyle = function(cell)
{
var stylename = this.model.getStyle(cell);
var style = null;
// Gets the default style for the cell
if (this.model.isEdge(cell))
{
style = this.stylesheet.getDefaultEdgeStyle();
}
else
{
style = this.stylesheet.getDefaultVertexStyle();
}
// Resolves the stylename using the above as the default
if (stylename != null)
{
style = this.postProcessCellStyle(this.stylesheet.getCellStyle(stylename, style));
}
// Returns a non-null value if no style can be found
if (style == null)
{
style = new Object();
}
return style;
};
/**
* Function: postProcessCellStyle
*
* Tries to resolve the value for the image style in the image bundles and
* turns short data URIs as defined in mxImageBundle to data URIs as
* defined in RFC 2397 of the IETF.
*/
mxGraph.prototype.postProcessCellStyle = function(style)
{
if (style != null)
{
var key = style[mxConstants.STYLE_IMAGE];
var image = this.getImageFromBundles(key);
if (image != null)
{
style[mxConstants.STYLE_IMAGE] = image;
}
else
{
image = key;
}
// Converts short data uris to normal data uris
if (image != null && image.substring(0, 11) == 'data:image/')
{
if (image.substring(0, 20) == 'data:image/svg+xml,<')
{
// Required for FF and IE11
image = image.substring(0, 19) + encodeURIComponent(image.substring(19));
}
else if (image.substring(0, 22) != 'data:image/svg+xml,%3C')
{
var comma = image.indexOf(',');
// Adds base64 encoding prefix if needed
if (comma > 0 && image.substring(comma - 7, comma + 1) != ';base64,')
{
image = image.substring(0, comma) + ';base64,'
+ image.substring(comma + 1);
}
}
style[mxConstants.STYLE_IMAGE] = image;
}
}
return style;
};
/**
* Function: setCellStyle
*
* Sets the style of the specified cells. If no cells are given, then the
* selection cells are changed.
*
* Parameters:
*
* style - String representing the new style of the cells.
* cells - Optional array of <mxCells> to set the style for. Default is the
* selection cells.
*/
mxGraph.prototype.setCellStyle = function(style, cells)
{
cells = cells || this.getSelectionCells();
if (cells != null)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
this.model.setStyle(cells[i], style);
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: toggleCellStyle
*
* Toggles the boolean value for the given key in the style of the given cell
* and returns the new value as 0 or 1. If no cell is specified then the
* selection cell is used.
*
* Parameter:
*
* key - String representing the key for the boolean value to be toggled.
* defaultValue - Optional boolean default value if no value is defined.
* Default is false.
* cell - Optional <mxCell> whose style should be modified. Default is
* the selection cell.
*/
mxGraph.prototype.toggleCellStyle = function(key, defaultValue, cell)
{
cell = cell || this.getSelectionCell();
return this.toggleCellStyles(key, defaultValue, [cell]);
};
/**
* Function: toggleCellStyles
*
* Toggles the boolean value for the given key in the style of the given cells
* and returns the new value as 0 or 1. If no cells are specified, then the
* selection cells are used. For example, this can be used to toggle
* <mxConstants.STYLE_ROUNDED> or any other style with a boolean value.
*
* Parameter:
*
* key - String representing the key for the boolean value to be toggled.
* defaultValue - Optional boolean default value if no value is defined.
* Default is false.
* cells - Optional array of <mxCells> whose styles should be modified.
* Default is the selection cells.
*/
mxGraph.prototype.toggleCellStyles = function(key, defaultValue, cells)
{
defaultValue = (defaultValue != null) ? defaultValue : false;
cells = cells || this.getSelectionCells();
var value = null;
if (cells != null && cells.length > 0)
{
var state = this.view.getState(cells[0]);
var style = (state != null) ? state.style : this.getCellStyle(cells[0]);
if (style != null)
{
value = (mxUtils.getValue(style, key, defaultValue)) ? 0 : 1;
this.setCellStyles(key, value, cells);
}
}
return value;
};
/**
* Function: setCellStyles
*
* Sets the key to value in the styles of the given cells. This will modify
* the existing cell styles in-place and override any existing assignment
* for the given key. If no cells are specified, then the selection cells
* are changed. If no value is specified, then the respective key is
* removed from the styles.
*
* Parameters:
*
* key - String representing the key to be assigned.
* value - String representing the new value for the key.
* cells - Optional array of <mxCells> to change the style for. Default is
* the selection cells.
*/
mxGraph.prototype.setCellStyles = function(key, value, cells)
{
cells = cells || this.getSelectionCells();
mxUtils.setCellStyles(this.model, cells, key, value);
};
/**
* Function: toggleCellStyleFlags
*
* Toggles the given bit for the given key in the styles of the specified
* cells.
*
* Parameters:
*
* key - String representing the key to toggle the flag in.
* flag - Integer that represents the bit to be toggled.
* cells - Optional array of <mxCells> to change the style for. Default is
* the selection cells.
*/
mxGraph.prototype.toggleCellStyleFlags = function(key, flag, cells)
{
this.setCellStyleFlags(key, flag, null, cells);
};
/**
* Function: setCellStyleFlags
*
* Sets or toggles the given bit for the given key in the styles of the
* specified cells.
*
* Parameters:
*
* key - String representing the key to toggle the flag in.
* flag - Integer that represents the bit to be toggled.
* value - Boolean value to be used or null if the value should be toggled.
* cells - Optional array of <mxCells> to change the style for. Default is
* the selection cells.
*/
mxGraph.prototype.setCellStyleFlags = function(key, flag, value, cells)
{
cells = cells || this.getSelectionCells();
if (cells != null && cells.length > 0)
{
if (value == null)
{
var state = this.view.getState(cells[0]);
var style = (state != null) ? state.style : this.getCellStyle(cells[0]);
if (style != null)
{
var current = parseInt(style[key] || 0);
value = !((current & flag) == flag);
}
}
mxUtils.setCellStyleFlags(this.model, cells, key, flag, value);
}
};
/**
* Group: Cell alignment and orientation
*/
/**
* Function: alignCells
*
* Aligns the given cells vertically or horizontally according to the given
* alignment using the optional parameter as the coordinate.
*
* Parameters:
*
* align - Specifies the alignment. Possible values are all constants in
* mxConstants with an ALIGN prefix.
* cells - Array of <mxCells> to be aligned.
* param - Optional coordinate for the alignment.
*/
mxGraph.prototype.alignCells = function(align, cells, param)
{
if (cells == null)
{
cells = this.getSelectionCells();
}
if (cells != null && cells.length > 1)
{
// Finds the required coordinate for the alignment
if (param == null)
{
for (var i = 0; i < cells.length; i++)
{
var state = this.view.getState(cells[i]);
if (state != null && !this.model.isEdge(cells[i]))
{
if (param == null)
{
if (align == mxConstants.ALIGN_CENTER)
{
param = state.x + state.width / 2;
break;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
param = state.x + state.width;
}
else if (align == mxConstants.ALIGN_TOP)
{
param = state.y;
}
else if (align == mxConstants.ALIGN_MIDDLE)
{
param = state.y + state.height / 2;
break;
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
param = state.y + state.height;
}
else
{
param = state.x;
}
}
else
{
if (align == mxConstants.ALIGN_RIGHT)
{
param = Math.max(param, state.x + state.width);
}
else if (align == mxConstants.ALIGN_TOP)
{
param = Math.min(param, state.y);
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
param = Math.max(param, state.y + state.height);
}
else
{
param = Math.min(param, state.x);
}
}
}
}
}
// Aligns the cells to the coordinate
if (param != null)
{
var s = this.view.scale;
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
var state = this.view.getState(cells[i]);
if (state != null)
{
var geo = this.getCellGeometry(cells[i]);
if (geo != null && !this.model.isEdge(cells[i]))
{
geo = geo.clone();
if (align == mxConstants.ALIGN_CENTER)
{
geo.x += (param - state.x - state.width / 2) / s;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
geo.x += (param - state.x - state.width) / s;
}
else if (align == mxConstants.ALIGN_TOP)
{
geo.y += (param - state.y) / s;
}
else if (align == mxConstants.ALIGN_MIDDLE)
{
geo.y += (param - state.y - state.height / 2) / s;
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
geo.y += (param - state.y - state.height) / s;
}
else
{
geo.x += (param - state.x) / s;
}
this.resizeCell(cells[i], geo);
}
}
}
this.fireEvent(new mxEventObject(mxEvent.ALIGN_CELLS,
'align', align, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
}
return cells;
};
/**
* Function: flipEdge
*
* Toggles the style of the given edge between null (or empty) and
* <alternateEdgeStyle>. This method fires <mxEvent.FLIP_EDGE> while the
* transaction is in progress. Returns the edge that was flipped.
*
* Here is an example that overrides this implementation to invert the
* value of <mxConstants.STYLE_ELBOW> without removing any existing styles.
*
* (code)
* graph.flipEdge = function(edge)
* {
* if (edge != null)
* {
* var state = this.view.getState(edge);
* var style = (state != null) ? state.style : this.getCellStyle(edge);
*
* if (style != null)
* {
* var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW,
* mxConstants.ELBOW_HORIZONTAL);
* var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ?
* mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL;
* this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]);
* }
* }
* };
* (end)
*
* Parameters:
*
* edge - <mxCell> whose style should be changed.
*/
mxGraph.prototype.flipEdge = function(edge)
{
if (edge != null &&
this.alternateEdgeStyle != null)
{
this.model.beginUpdate();
try
{
var style = this.model.getStyle(edge);
if (style == null || style.length == 0)
{
this.model.setStyle(edge, this.alternateEdgeStyle);
}
else
{
this.model.setStyle(edge, null);
}
// Removes all existing control points
this.resetEdge(edge);
this.fireEvent(new mxEventObject(mxEvent.FLIP_EDGE, 'edge', edge));
}
finally
{
this.model.endUpdate();
}
}
return edge;
};
/**
* Function: addImageBundle
*
* Adds the specified <mxImageBundle>.
*/
mxGraph.prototype.addImageBundle = function(bundle)
{
this.imageBundles.push(bundle);
};
/**
* Function: removeImageBundle
*
* Removes the specified <mxImageBundle>.
*/
mxGraph.prototype.removeImageBundle = function(bundle)
{
var tmp = [];
for (var i = 0; i < this.imageBundles.length; i++)
{
if (this.imageBundles[i] != bundle)
{
tmp.push(this.imageBundles[i]);
}
}
this.imageBundles = tmp;
};
/**
* Function: getImageFromBundles
*
* Searches all <imageBundles> for the specified key and returns the value
* for the first match or null if the key is not found.
*/
mxGraph.prototype.getImageFromBundles = function(key)
{
if (key != null)
{
for (var i = 0; i < this.imageBundles.length; i++)
{
var image = this.imageBundles[i].getImage(key);
if (image != null)
{
return image;
}
}
}
return null;
};
/**
* Group: Order
*/
/**
* Function: orderCells
*
* Moves the given cells to the front or back. The change is carried out
* using <cellsOrdered>. This method fires <mxEvent.ORDER_CELLS> while the
* transaction is in progress.
*
* Parameters:
*
* back - Boolean that specifies if the cells should be moved to back.
* cells - Array of <mxCells> to move to the background. If null is
* specified then the selection cells are used.
*/
mxGraph.prototype.orderCells = function(back, cells)
{
if (cells == null)
{
cells = mxUtils.sortCells(this.getSelectionCells(), true);
}
this.model.beginUpdate();
try
{
this.cellsOrdered(cells, back);
this.fireEvent(new mxEventObject(mxEvent.ORDER_CELLS,
'back', back, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsOrdered
*
* Moves the given cells to the front or back. This method fires
* <mxEvent.CELLS_ORDERED> while the transaction is in progress.
*
* Parameters:
*
* cells - Array of <mxCells> whose order should be changed.
* back - Boolean that specifies if the cells should be moved to back.
*/
mxGraph.prototype.cellsOrdered = function(cells, back)
{
if (cells != null)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
var parent = this.model.getParent(cells[i]);
if (back)
{
this.model.add(parent, cells[i], i);
}
else
{
this.model.add(parent, cells[i],
this.model.getChildCount(parent) - 1);
}
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_ORDERED,
'back', back, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Group: Grouping
*/
/**
* Function: groupCells
*
* Adds the cells into the given group. The change is carried out using
* <cellsAdded>, <cellsMoved> and <cellsResized>. This method fires
* <mxEvent.GROUP_CELLS> while the transaction is in progress. Returns the
* new group. A group is only created if there is at least one entry in the
* given array of cells.
*
* Parameters:
*
* group - <mxCell> that represents the target group. If null is specified
* then a new group is created using <createGroupCell>.
* border - Optional integer that specifies the border between the child
* area and the group bounds. Default is 0.
* cells - Optional array of <mxCells> to be grouped. If null is specified
* then the selection cells are used.
*/
mxGraph.prototype.groupCells = function(group, border, cells)
{
if (cells == null)
{
cells = mxUtils.sortCells(this.getSelectionCells(), true);
}
cells = this.getCellsForGroup(cells);
if (group == null)
{
group = this.createGroupCell(cells);
}
var bounds = this.getBoundsForGroup(group, cells, border);
if (cells.length > 0 && bounds != null)
{
// Uses parent of group or previous parent of first child
var parent = this.model.getParent(group);
if (parent == null)
{
parent = this.model.getParent(cells[0]);
}
this.model.beginUpdate();
try
{
// Checks if the group has a geometry and
// creates one if one does not exist
if (this.getCellGeometry(group) == null)
{
this.model.setGeometry(group, new mxGeometry());
}
// Adds the group into the parent
var index = this.model.getChildCount(parent);
this.cellsAdded([group], parent, index, null, null, false, false, false);
// Adds the children into the group and moves
index = this.model.getChildCount(group);
this.cellsAdded(cells, group, index, null, null, false, false, false);
this.cellsMoved(cells, -bounds.x, -bounds.y, false, false, false);
// Resizes the group
this.cellsResized([group], [bounds], false);
this.fireEvent(new mxEventObject(mxEvent.GROUP_CELLS,
'group', group, 'border', border, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
return group;
};
/**
* Function: getCellsForGroup
*
* Returns the cells with the same parent as the first cell
* in the given array.
*/
mxGraph.prototype.getCellsForGroup = function(cells)
{
var result = [];
if (cells != null && cells.length > 0)
{
var parent = this.model.getParent(cells[0]);
result.push(cells[0]);
// Filters selection cells with the same parent
for (var i = 1; i < cells.length; i++)
{
if (this.model.getParent(cells[i]) == parent)
{
result.push(cells[i]);
}
}
}
return result;
};
/**
* Function: getBoundsForGroup
*
* Returns the bounds to be used for the given group and children.
*/
mxGraph.prototype.getBoundsForGroup = function(group, children, border)
{
var result = this.getBoundingBoxFromGeometry(children, true);
if (result != null)
{
if (this.isSwimlane(group))
{
var size = this.getStartSize(group);
result.x -= size.width;
result.y -= size.height;
result.width += size.width;
result.height += size.height;
}
// Adds the border
if (border != null)
{
result.x -= border;
result.y -= border;
result.width += 2 * border;
result.height += 2 * border;
}
}
return result;
};
/**
* Function: createGroupCell
*
* Hook for creating the group cell to hold the given array of <mxCells> if
* no group cell was given to the <group> function.
*
* The following code can be used to set the style of new group cells.
*
* (code)
* var graphCreateGroupCell = graph.createGroupCell;
* graph.createGroupCell = function(cells)
* {
* var group = graphCreateGroupCell.apply(this, arguments);
* group.setStyle('group');
*
* return group;
* };
*/
mxGraph.prototype.createGroupCell = function(cells)
{
var group = new mxCell('');
group.setVertex(true);
group.setConnectable(false);
return group;
};
/**
* Function: ungroupCells
*
* Ungroups the given cells by moving the children the children to their
* parents parent and removing the empty groups. Returns the children that
* have been removed from the groups.
*
* Parameters:
*
* cells - Array of cells to be ungrouped. If null is specified then the
* selection cells are used.
*/
mxGraph.prototype.ungroupCells = function(cells)
{
var result = [];
if (cells == null)
{
cells = this.getSelectionCells();
// Finds the cells with children
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
if (this.model.getChildCount(cells[i]) > 0)
{
tmp.push(cells[i]);
}
}
cells = tmp;
}
if (cells != null && cells.length > 0)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
var children = this.model.getChildren(cells[i]);
if (children != null && children.length > 0)
{
children = children.slice();
var parent = this.model.getParent(cells[i]);
var index = this.model.getChildCount(parent);
this.cellsAdded(children, parent, index, null, null, true);
result = result.concat(children);
}
}
this.removeCellsAfterUngroup(cells);
this.fireEvent(new mxEventObject(mxEvent.UNGROUP_CELLS, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
return result;
};
/**
* Function: removeCellsAfterUngroup
*
* Hook to remove the groups after <ungroupCells>.
*
* Parameters:
*
* cells - Array of <mxCells> that were ungrouped.
*/
mxGraph.prototype.removeCellsAfterUngroup = function(cells)
{
this.cellsRemoved(this.addAllEdges(cells));
};
/**
* Function: removeCellsFromParent
*
* Removes the specified cells from their parents and adds them to the
* default parent. Returns the cells that were removed from their parents.
*
* Parameters:
*
* cells - Array of <mxCells> to be removed from their parents.
*/
mxGraph.prototype.removeCellsFromParent = function(cells)
{
if (cells == null)
{
cells = this.getSelectionCells();
}
this.model.beginUpdate();
try
{
var parent = this.getDefaultParent();
var index = this.model.getChildCount(parent);
this.cellsAdded(cells, parent, index, null, null, true);
this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS_FROM_PARENT, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: updateGroupBounds
*
* Updates the bounds of the given groups to include all children and returns
* the passed-in cells. Call this with the groups in parent to child order,
* top-most group first, the cells are processed in reverse order and cells
* with no children are ignored.
*
* Parameters:
*
* cells - The groups whose bounds should be updated. If this is null, then
* the selection cells are used.
* border - Optional border to be added in the group. Default is 0.
* moveGroup - Optional boolean that allows the group to be moved. Default
* is false.
* topBorder - Optional top border to be added in the group. Default is 0.
* rightBorder - Optional top border to be added in the group. Default is 0.
* bottomBorder - Optional top border to be added in the group. Default is 0.
* leftBorder - Optional top border to be added in the group. Default is 0.
*/
mxGraph.prototype.updateGroupBounds = function(cells, border, moveGroup, topBorder, rightBorder, bottomBorder, leftBorder)
{
if (cells == null)
{
cells = this.getSelectionCells();
}
border = (border != null) ? border : 0;
moveGroup = (moveGroup != null) ? moveGroup : false;
topBorder = (topBorder != null) ? topBorder : 0;
rightBorder = (rightBorder != null) ? rightBorder : 0;
bottomBorder = (bottomBorder != null) ? bottomBorder : 0;
leftBorder = (leftBorder != null) ? leftBorder : 0;
this.model.beginUpdate();
try
{
for (var i = cells.length - 1; i >= 0; i--)
{
var geo = this.getCellGeometry(cells[i]);
if (geo != null)
{
var children = this.getChildCells(cells[i]);
if (children != null && children.length > 0)
{
var bounds = this.getBoundingBoxFromGeometry(children, true);
if (bounds != null && bounds.width > 0 && bounds.height > 0)
{
var left = 0;
var top = 0;
// Adds the size of the title area for swimlanes
if (this.isSwimlane(cells[i]))
{
var size = this.getStartSize(cells[i]);
left = size.width;
top = size.height;
}
geo = geo.clone();
if (moveGroup)
{
geo.x = Math.round(geo.x + bounds.x - border - left - leftBorder);
geo.y = Math.round(geo.y + bounds.y - border - top - topBorder);
}
geo.width = Math.round(bounds.width + 2 * border + left + leftBorder + rightBorder);
geo.height = Math.round(bounds.height + 2 * border + top + topBorder + bottomBorder);
this.model.setGeometry(cells[i], geo);
this.moveCells(children, border + left - bounds.x + leftBorder,
border + top - bounds.y + topBorder);
}
}
}
}
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: getBoundingBox
*
* Returns the bounding box for the given array of <mxCells>. The bounding box for
* each cell and its descendants is computed using <mxGraphView.getBoundingBox>.
*
* Parameters:
*
* cells - Array of <mxCells> whose bounding box should be returned.
*/
mxGraph.prototype.getBoundingBox = function(cells)
{
var result = null;
if (cells != null && cells.length > 0)
{
for (var i = 0; i < cells.length; i++)
{
if (this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
{
var bbox = this.view.getBoundingBox(this.view.getState(cells[i]), true);
if (bbox != null)
{
if (result == null)
{
result = mxRectangle.fromRectangle(bbox);
}
else
{
result.add(bbox);
}
}
}
}
}
return result;
};
/**
* Group: Cell cloning, insertion and removal
*/
/**
* Function: cloneCell
*
* Returns the clone for the given cell. Uses <cloneCells>.
*
* Parameters:
*
* cell - <mxCell> to be cloned.
* allowInvalidEdges - Optional boolean that specifies if invalid edges
* should be cloned. Default is true.
* mapping - Optional mapping for existing clones.
* keepPosition - Optional boolean indicating if the position of the cells should
* be updated to reflect the lost parent cell. Default is false.
*/
mxGraph.prototype.cloneCell = function(cell, allowInvalidEdges, mapping, keepPosition)
{
return this.cloneCells([cell], allowInvalidEdges, mapping, keepPosition)[0];
};
/**
* Function: cloneCells
*
* Returns the clones for the given cells. The clones are created recursively
* using <mxGraphModel.cloneCells>. If the terminal of an edge is not in the
* given array, then the respective end is assigned a terminal point and the
* terminal is removed.
*
* Parameters:
*
* cells - Array of <mxCells> to be cloned.
* allowInvalidEdges - Optional boolean that specifies if invalid edges
* should be cloned. Default is true.
* mapping - Optional mapping for existing clones.
* keepPosition - Optional boolean indicating if the position of the cells should
* be updated to reflect the lost parent cell. Default is false.
*/
mxGraph.prototype.cloneCells = function(cells, allowInvalidEdges, mapping, keepPosition)
{
allowInvalidEdges = (allowInvalidEdges != null) ? allowInvalidEdges : true;
var clones = null;
if (cells != null)
{
// Creates a dictionary for fast lookups
var dict = new mxDictionary();
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
tmp.push(cells[i]);
}
if (tmp.length > 0)
{
var scale = this.view.scale;
var trans = this.view.translate;
clones = this.model.cloneCells(cells, true, mapping);
for (var i = 0; i < cells.length; i++)
{
if (!allowInvalidEdges && this.model.isEdge(clones[i]) &&
this.getEdgeValidationError(clones[i],
this.model.getTerminal(clones[i], true),
this.model.getTerminal(clones[i], false)) != null)
{
clones[i] = null;
}
else
{
var g = this.model.getGeometry(clones[i]);
if (g != null)
{
var state = this.view.getState(cells[i]);
var pstate = this.view.getState(this.model.getParent(cells[i]));
if (state != null && pstate != null)
{
var dx = (keepPosition) ? 0 : pstate.origin.x;
var dy = (keepPosition) ? 0 : pstate.origin.y;
if (this.model.isEdge(clones[i]))
{
var pts = state.absolutePoints;
if (pts != null)
{
// Checks if the source is cloned or sets the terminal point
var src = this.model.getTerminal(cells[i], true);
while (src != null && !dict.get(src))
{
src = this.model.getParent(src);
}
if (src == null && pts[0] != null)
{
g.setTerminalPoint(
new mxPoint(pts[0].x / scale - trans.x,
pts[0].y / scale - trans.y), true);
}
// Checks if the target is cloned or sets the terminal point
var trg = this.model.getTerminal(cells[i], false);
while (trg != null && !dict.get(trg))
{
trg = this.model.getParent(trg);
}
var n = pts.length - 1;
if (trg == null && pts[n] != null)
{
g.setTerminalPoint(
new mxPoint(pts[n].x / scale - trans.x,
pts[n].y / scale - trans.y), false);
}
// Translates the control points
var points = g.points;
if (points != null)
{
for (var j = 0; j < points.length; j++)
{
points[j].x += dx;
points[j].y += dy;
}
}
}
}
else
{
g.translate(dx, dy);
}
}
}
}
}
}
else
{
clones = [];
}
}
return clones;
};
/**
* Function: insertVertex
*
* Adds a new vertex into the given parent <mxCell> using value as the user
* object and the given coordinates as the <mxGeometry> of the new vertex.
* The id and style are used for the respective properties of the new
* <mxCell>, which is returned.
*
* When adding new vertices from a mouse event, one should take into
* account the offset of the graph container and the scale and translation
* of the view in order to find the correct unscaled, untranslated
* coordinates using <mxGraph.getPointForEvent> as follows:
*
* (code)
* var pt = graph.getPointForEvent(evt);
* var parent = graph.getDefaultParent();
* graph.insertVertex(parent, null,
* 'Hello, World!', x, y, 220, 30);
* (end)
*
* For adding image cells, the style parameter can be assigned as
*
* (code)
* stylename;image=imageUrl
* (end)
*
* See <mxGraph> for more information on using images.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent of the new vertex.
* id - Optional string that defines the Id of the new vertex.
* value - Object to be used as the user object.
* x - Integer that defines the x coordinate of the vertex.
* y - Integer that defines the y coordinate of the vertex.
* width - Integer that defines the width of the vertex.
* height - Integer that defines the height of the vertex.
* style - Optional string that defines the cell style.
* relative - Optional boolean that specifies if the geometry is relative.
* Default is false.
*/
mxGraph.prototype.insertVertex = function(parent, id, value,
x, y, width, height, style, relative)
{
var vertex = this.createVertex(parent, id, value, x, y, width, height, style, relative);
return this.addCell(vertex, parent);
};
/**
* Function: createVertex
*
* Hook method that creates the new vertex for <insertVertex>.
*/
mxGraph.prototype.createVertex = function(parent, id, value,
x, y, width, height, style, relative)
{
// Creates the geometry for the vertex
var geometry = new mxGeometry(x, y, width, height);
geometry.relative = (relative != null) ? relative : false;
// Creates the vertex
var vertex = new mxCell(value, geometry, style);
vertex.setId(id);
vertex.setVertex(true);
vertex.setConnectable(true);
return vertex;
};
/**
* Function: insertEdge
*
* Adds a new edge into the given parent <mxCell> using value as the user
* object and the given source and target as the terminals of the new edge.
* The id and style are used for the respective properties of the new
* <mxCell>, which is returned.
*
* Parameters:
*
* parent - <mxCell> that specifies the parent of the new edge.
* id - Optional string that defines the Id of the new edge.
* value - JavaScript object to be used as the user object.
* source - <mxCell> that defines the source of the edge.
* target - <mxCell> that defines the target of the edge.
* style - Optional string that defines the cell style.
*/
mxGraph.prototype.insertEdge = function(parent, id, value, source, target, style)
{
var edge = this.createEdge(parent, id, value, source, target, style);
return this.addEdge(edge, parent, source, target);
};
/**
* Function: createEdge
*
* Hook method that creates the new edge for <insertEdge>. This
* implementation does not set the source and target of the edge, these
* are set when the edge is added to the model.
*
*/
mxGraph.prototype.createEdge = function(parent, id, value, source, target, style)
{
// Creates the edge
var edge = new mxCell(value, new mxGeometry(), style);
edge.setId(id);
edge.setEdge(true);
edge.geometry.relative = true;
return edge;
};
/**
* Function: addEdge
*
* Adds the edge to the parent and connects it to the given source and
* target terminals. This is a shortcut method. Returns the edge that was
* added.
*
* Parameters:
*
* edge - <mxCell> to be inserted into the given parent.
* parent - <mxCell> that represents the new parent. If no parent is
* given then the default parent is used.
* source - Optional <mxCell> that represents the source terminal.
* target - Optional <mxCell> that represents the target terminal.
* index - Optional index to insert the cells at. Default is to append.
*/
mxGraph.prototype.addEdge = function(edge, parent, source, target, index)
{
return this.addCell(edge, parent, index, source, target);
};
/**
* Function: addCell
*
* Adds the cell to the parent and connects it to the given source and
* target terminals. This is a shortcut method. Returns the cell that was
* added.
*
* Parameters:
*
* cell - <mxCell> to be inserted into the given parent.
* parent - <mxCell> that represents the new parent. If no parent is
* given then the default parent is used.
* index - Optional index to insert the cells at. Default is to append.
* source - Optional <mxCell> that represents the source terminal.
* target - Optional <mxCell> that represents the target terminal.
*/
mxGraph.prototype.addCell = function(cell, parent, index, source, target)
{
return this.addCells([cell], parent, index, source, target)[0];
};
/**
* Function: addCells
*
* Adds the cells to the parent at the given index, connecting each cell to
* the optional source and target terminal. The change is carried out using
* <cellsAdded>. This method fires <mxEvent.ADD_CELLS> while the
* transaction is in progress. Returns the cells that were added.
*
* Parameters:
*
* cells - Array of <mxCells> to be inserted.
* parent - <mxCell> that represents the new parent. If no parent is
* given then the default parent is used.
* index - Optional index to insert the cells at. Default is to append.
* source - Optional source <mxCell> for all inserted cells.
* target - Optional target <mxCell> for all inserted cells.
*/
mxGraph.prototype.addCells = function(cells, parent, index, source, target)
{
if (parent == null)
{
parent = this.getDefaultParent();
}
if (index == null)
{
index = this.model.getChildCount(parent);
}
this.model.beginUpdate();
try
{
this.cellsAdded(cells, parent, index, source, target, false, true);
this.fireEvent(new mxEventObject(mxEvent.ADD_CELLS, 'cells', cells,
'parent', parent, 'index', index, 'source', source, 'target', target));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsAdded
*
* Adds the specified cells to the given parent. This method fires
* <mxEvent.CELLS_ADDED> while the transaction is in progress.
*/
mxGraph.prototype.cellsAdded = function(cells, parent, index, source, target, absolute, constrain, extend)
{
if (cells != null && parent != null && index != null)
{
this.model.beginUpdate();
try
{
var parentState = (absolute) ? this.view.getState(parent) : null;
var o1 = (parentState != null) ? parentState.origin : null;
var zero = new mxPoint(0, 0);
for (var i = 0; i < cells.length; i++)
{
if (cells[i] == null)
{
index--;
}
else
{
var previous = this.model.getParent(cells[i]);
// Keeps the cell at its absolute location
if (o1 != null && cells[i] != parent && parent != previous)
{
var oldState = this.view.getState(previous);
var o2 = (oldState != null) ? oldState.origin : zero;
var geo = this.model.getGeometry(cells[i]);
if (geo != null)
{
var dx = o2.x - o1.x;
var dy = o2.y - o1.y;
// FIXME: Cells should always be inserted first before any other edit
// to avoid forward references in sessions.
geo = geo.clone();
geo.translate(dx, dy);
if (!geo.relative && this.model.isVertex(cells[i]) &&
!this.isAllowNegativeCoordinates())
{
geo.x = Math.max(0, geo.x);
geo.y = Math.max(0, geo.y);
}
this.model.setGeometry(cells[i], geo);
}
}
// Decrements all following indices
// if cell is already in parent
if (parent == previous && index + i > this.model.getChildCount(parent))
{
index--;
}
this.model.add(parent, cells[i], index + i);
if (this.autoSizeCellsOnAdd)
{
this.autoSizeCell(cells[i], true);
}
// Extends the parent or constrains the child
if ((extend == null || extend) &&
this.isExtendParentsOnAdd(cells[i]) && this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
// Additionally constrains the child after extending the parent
if (constrain == null || constrain)
{
this.constrainChild(cells[i]);
}
// Sets the source terminal
if (source != null)
{
this.cellConnected(cells[i], source, true);
}
// Sets the target terminal
if (target != null)
{
this.cellConnected(cells[i], target, false);
}
}
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_ADDED, 'cells', cells,
'parent', parent, 'index', index, 'source', source, 'target', target,
'absolute', absolute));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: autoSizeCell
*
* Resizes the specified cell to just fit around the its label and/or children
*
* Parameters:
*
* cell - <mxCells> to be resized.
* recurse - Optional boolean which specifies if all descendants should be
* autosized. Default is true.
*/
mxGraph.prototype.autoSizeCell = function(cell, recurse)
{
recurse = (recurse != null) ? recurse : true;
if (recurse)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.autoSizeCell(this.model.getChildAt(cell, i));
}
}
if (this.getModel().isVertex(cell) && this.isAutoSizeCell(cell))
{
this.updateCellSize(cell);
}
};
/**
* Function: removeCells
*
* Removes the given cells from the graph including all connected edges if
* includeEdges is true. The change is carried out using <cellsRemoved>.
* This method fires <mxEvent.REMOVE_CELLS> while the transaction is in
* progress. The removed cells are returned as an array.
*
* Parameters:
*
* cells - Array of <mxCells> to remove. If null is specified then the
* selection cells which are deletable are used.
* includeEdges - Optional boolean which specifies if all connected edges
* should be removed as well. Default is true.
*/
mxGraph.prototype.removeCells = function(cells, includeEdges)
{
includeEdges = (includeEdges != null) ? includeEdges : true;
if (cells == null)
{
cells = this.getDeletableCells(this.getSelectionCells());
}
// Adds all edges to the cells
if (includeEdges)
{
// FIXME: Remove duplicate cells in result or do not add if
// in cells or descendant of cells
cells = this.getDeletableCells(this.addAllEdges(cells));
}
else
{
cells = cells.slice();
// Removes edges that are currently not
// visible as those cannot be updated
var edges = this.getDeletableCells(this.getAllEdges(cells));
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
for (var i = 0; i < edges.length; i++)
{
if (this.view.getState(edges[i]) == null &&
!dict.get(edges[i]))
{
dict.put(edges[i], true);
cells.push(edges[i]);
}
}
}
this.model.beginUpdate();
try
{
this.cellsRemoved(cells);
this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS,
'cells', cells, 'includeEdges', includeEdges));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsRemoved
*
* Removes the given cells from the model. This method fires
* <mxEvent.CELLS_REMOVED> while the transaction is in progress.
*
* Parameters:
*
* cells - Array of <mxCells> to remove.
*/
mxGraph.prototype.cellsRemoved = function(cells)
{
if (cells != null && cells.length > 0)
{
var scale = this.view.scale;
var tr = this.view.translate;
this.model.beginUpdate();
try
{
// Creates hashtable for faster lookup
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
for (var i = 0; i < cells.length; i++)
{
// Disconnects edges which are not being removed
var edges = this.getAllEdges([cells[i]]);
var disconnectTerminal = mxUtils.bind(this, function(edge, source)
{
var geo = this.model.getGeometry(edge);
if (geo != null)
{
// Checks if terminal is being removed
var terminal = this.model.getTerminal(edge, source);
var connected = false;
var tmp = terminal;
while (tmp != null)
{
if (cells[i] == tmp)
{
connected = true;
break;
}
tmp = this.model.getParent(tmp);
}
if (connected)
{
geo = geo.clone();
var state = this.view.getState(edge);
if (state != null && state.absolutePoints != null)
{
var pts = state.absolutePoints;
var n = (source) ? 0 : pts.length - 1;
geo.setTerminalPoint(new mxPoint(
pts[n].x / scale - tr.x - state.origin.x,
pts[n].y / scale - tr.y - state.origin.y), source);
}
else
{
// Fallback to center of terminal if routing
// points are not available to add new point
// KNOWN: Should recurse to find parent offset
// of edge for nested groups but invisible edges
// should be removed in removeCells step
var tstate = this.view.getState(terminal);
if (tstate != null)
{
geo.setTerminalPoint(new mxPoint(
tstate.getCenterX() / scale - tr.x,
tstate.getCenterY() / scale - tr.y), source);
}
}
this.model.setGeometry(edge, geo);
this.model.setTerminal(edge, null, source);
}
}
});
for (var j = 0; j < edges.length; j++)
{
if (!dict.get(edges[j]))
{
dict.put(edges[j], true);
disconnectTerminal(edges[j], true);
disconnectTerminal(edges[j], false);
}
}
this.model.remove(cells[i]);
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_REMOVED, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: splitEdge
*
* Splits the given edge by adding the newEdge between the previous source
* and the given cell and reconnecting the source of the given edge to the
* given cell. This method fires <mxEvent.SPLIT_EDGE> while the transaction
* is in progress. Returns the new edge that was inserted.
*
* Parameters:
*
* edge - <mxCell> that represents the edge to be splitted.
* cells - <mxCells> that represents the cells to insert into the edge.
* newEdge - <mxCell> that represents the edge to be inserted.
* dx - Optional integer that specifies the vector to move the cells.
* dy - Optional integer that specifies the vector to move the cells.
*/
mxGraph.prototype.splitEdge = function(edge, cells, newEdge, dx, dy)
{
dx = dx || 0;
dy = dy || 0;
var parent = this.model.getParent(edge);
var source = this.model.getTerminal(edge, true);
this.model.beginUpdate();
try
{
if (newEdge == null)
{
newEdge = this.cloneCell(edge);
// Removes waypoints before/after new cell
var state = this.view.getState(edge);
var geo = this.getCellGeometry(newEdge);
if (geo != null && geo.points != null && state != null)
{
var t = this.view.translate;
var s = this.view.scale;
var idx = mxUtils.findNearestSegment(state, (dx + t.x) * s, (dy + t.y) * s);
geo.points = geo.points.slice(0, idx);
geo = this.getCellGeometry(edge);
if (geo != null && geo.points != null)
{
geo = geo.clone();
geo.points = geo.points.slice(idx);
this.model.setGeometry(edge, geo);
}
}
}
this.cellsMoved(cells, dx, dy, false, false);
this.cellsAdded(cells, parent, this.model.getChildCount(parent), null, null,
true);
this.cellsAdded([newEdge], parent, this.model.getChildCount(parent),
source, cells[0], false);
this.cellConnected(edge, cells[0], true);
this.fireEvent(new mxEventObject(mxEvent.SPLIT_EDGE, 'edge', edge,
'cells', cells, 'newEdge', newEdge, 'dx', dx, 'dy', dy));
}
finally
{
this.model.endUpdate();
}
return newEdge;
};
/**
* Group: Cell visibility
*/
/**
* Function: toggleCells
*
* Sets the visible state of the specified cells and all connected edges
* if includeEdges is true. The change is carried out using <cellsToggled>.
* This method fires <mxEvent.TOGGLE_CELLS> while the transaction is in
* progress. Returns the cells whose visible state was changed.
*
* Parameters:
*
* show - Boolean that specifies the visible state to be assigned.
* cells - Array of <mxCells> whose visible state should be changed. If
* null is specified then the selection cells are used.
* includeEdges - Optional boolean indicating if the visible state of all
* connected edges should be changed as well. Default is true.
*/
mxGraph.prototype.toggleCells = function(show, cells, includeEdges)
{
if (cells == null)
{
cells = this.getSelectionCells();
}
// Adds all connected edges recursively
if (includeEdges)
{
cells = this.addAllEdges(cells);
}
this.model.beginUpdate();
try
{
this.cellsToggled(cells, show);
this.fireEvent(new mxEventObject(mxEvent.TOGGLE_CELLS,
'show', show, 'cells', cells, 'includeEdges', includeEdges));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsToggled
*
* Sets the visible state of the specified cells.
*
* Parameters:
*
* cells - Array of <mxCells> whose visible state should be changed.
* show - Boolean that specifies the visible state to be assigned.
*/
mxGraph.prototype.cellsToggled = function(cells, show)
{
if (cells != null && cells.length > 0)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
this.model.setVisible(cells[i], show);
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Group: Folding
*/
/**
* Function: foldCells
*
* Sets the collapsed state of the specified cells and all descendants
* if recurse is true. The change is carried out using <cellsFolded>.
* This method fires <mxEvent.FOLD_CELLS> while the transaction is in
* progress. Returns the cells whose collapsed state was changed.
*
* Parameters:
*
* collapsed - Boolean indicating the collapsed state to be assigned.
* recurse - Optional boolean indicating if the collapsed state of all
* descendants should be set. Default is false.
* cells - Array of <mxCells> whose collapsed state should be set. If
* null is specified then the foldable selection cells are used.
* checkFoldable - Optional boolean indicating of isCellFoldable should be
* checked. Default is false.
* evt - Optional native event that triggered the invocation.
*/
mxGraph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable, evt)
{
recurse = (recurse != null) ? recurse : false;
if (cells == null)
{
cells = this.getFoldableCells(this.getSelectionCells(), collapse);
}
this.stopEditing(false);
this.model.beginUpdate();
try
{
this.cellsFolded(cells, collapse, recurse, checkFoldable);
this.fireEvent(new mxEventObject(mxEvent.FOLD_CELLS,
'collapse', collapse, 'recurse', recurse, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsFolded
*
* Sets the collapsed state of the specified cells. This method fires
* <mxEvent.CELLS_FOLDED> while the transaction is in progress. Returns the
* cells whose collapsed state was changed.
*
* Parameters:
*
* cells - Array of <mxCells> whose collapsed state should be set.
* collapsed - Boolean indicating the collapsed state to be assigned.
* recurse - Boolean indicating if the collapsed state of all descendants
* should be set.
* checkFoldable - Optional boolean indicating of isCellFoldable should be
* checked. Default is false.
*/
mxGraph.prototype.cellsFolded = function(cells, collapse, recurse, checkFoldable)
{
if (cells != null && cells.length > 0)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
if ((!checkFoldable || this.isCellFoldable(cells[i], collapse)) &&
collapse != this.isCellCollapsed(cells[i]))
{
this.model.setCollapsed(cells[i], collapse);
this.swapBounds(cells[i], collapse);
if (this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
if (recurse)
{
var children = this.model.getChildren(cells[i]);
this.cellsFolded(children, collapse, recurse);
}
this.constrainChild(cells[i]);
}
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_FOLDED,
'cells', cells, 'collapse', collapse, 'recurse', recurse));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: swapBounds
*
* Swaps the alternate and the actual bounds in the geometry of the given
* cell invoking <updateAlternateBounds> before carrying out the swap.
*
* Parameters:
*
* cell - <mxCell> for which the bounds should be swapped.
* willCollapse - Boolean indicating if the cell is going to be collapsed.
*/
mxGraph.prototype.swapBounds = function(cell, willCollapse)
{
if (cell != null)
{
var geo = this.model.getGeometry(cell);
if (geo != null)
{
geo = geo.clone();
this.updateAlternateBounds(cell, geo, willCollapse);
geo.swap();
this.model.setGeometry(cell, geo);
}
}
};
/**
* Function: updateAlternateBounds
*
* Updates or sets the alternate bounds in the given geometry for the given
* cell depending on whether the cell is going to be collapsed. If no
* alternate bounds are defined in the geometry and
* <collapseToPreferredSize> is true, then the preferred size is used for
* the alternate bounds. The top, left corner is always kept at the same
* location.
*
* Parameters:
*
* cell - <mxCell> for which the geometry is being udpated.
* g - <mxGeometry> for which the alternate bounds should be updated.
* willCollapse - Boolean indicating if the cell is going to be collapsed.
*/
mxGraph.prototype.updateAlternateBounds = function(cell, geo, willCollapse)
{
if (cell != null && geo != null)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
if (geo.alternateBounds == null)
{
var bounds = geo;
if (this.collapseToPreferredSize)
{
var tmp = this.getPreferredSizeForCell(cell);
if (tmp != null)
{
bounds = tmp;
var startSize = mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE);
if (startSize > 0)
{
bounds.height = Math.max(bounds.height, startSize);
}
}
}
geo.alternateBounds = new mxRectangle(0, 0, bounds.width, bounds.height);
}
if (geo.alternateBounds != null)
{
geo.alternateBounds.x = geo.x;
geo.alternateBounds.y = geo.y;
var alpha = mxUtils.toRadians(style[mxConstants.STYLE_ROTATION] || 0);
if (alpha != 0)
{
var dx = geo.alternateBounds.getCenterX() - geo.getCenterX();
var dy = geo.alternateBounds.getCenterY() - geo.getCenterY();
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var dx2 = cos * dx - sin * dy;
var dy2 = sin * dx + cos * dy;
geo.alternateBounds.x += dx2 - dx;
geo.alternateBounds.y += dy2 - dy;
}
}
}
};
/**
* Function: addAllEdges
*
* Returns an array with the given cells and all edges that are connected
* to a cell or one of its descendants.
*/
mxGraph.prototype.addAllEdges = function(cells)
{
var allCells = cells.slice();
return mxUtils.removeDuplicates(allCells.concat(this.getAllEdges(cells)));
};
/**
* Function: getAllEdges
*
* Returns all edges connected to the given cells or its descendants.
*/
mxGraph.prototype.getAllEdges = function(cells)
{
var edges = [];
if (cells != null)
{
for (var i = 0; i < cells.length; i++)
{
var edgeCount = this.model.getEdgeCount(cells[i]);
for (var j = 0; j < edgeCount; j++)
{
edges.push(this.model.getEdgeAt(cells[i], j));
}
// Recurses
var children = this.model.getChildren(cells[i]);
edges = edges.concat(this.getAllEdges(children));
}
}
return edges;
};
/**
* Group: Cell sizing
*/
/**
* Function: updateCellSize
*
* Updates the size of the given cell in the model using <cellSizeUpdated>.
* This method fires <mxEvent.UPDATE_CELL_SIZE> while the transaction is in
* progress. Returns the cell whose size was updated.
*
* Parameters:
*
* cell - <mxCell> whose size should be updated.
*/
mxGraph.prototype.updateCellSize = function(cell, ignoreChildren)
{
ignoreChildren = (ignoreChildren != null) ? ignoreChildren : false;
this.model.beginUpdate();
try
{
this.cellSizeUpdated(cell, ignoreChildren);
this.fireEvent(new mxEventObject(mxEvent.UPDATE_CELL_SIZE,
'cell', cell, 'ignoreChildren', ignoreChildren));
}
finally
{
this.model.endUpdate();
}
return cell;
};
/**
* Function: cellSizeUpdated
*
* Updates the size of the given cell in the model using
* <getPreferredSizeForCell> to get the new size.
*
* Parameters:
*
* cell - <mxCell> for which the size should be changed.
*/
mxGraph.prototype.cellSizeUpdated = function(cell, ignoreChildren)
{
if (cell != null)
{
this.model.beginUpdate();
try
{
var size = this.getPreferredSizeForCell(cell);
var geo = this.model.getGeometry(cell);
if (size != null && geo != null)
{
var collapsed = this.isCellCollapsed(cell);
geo = geo.clone();
if (this.isSwimlane(cell))
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
var cellStyle = this.model.getStyle(cell);
if (cellStyle == null)
{
cellStyle = '';
}
if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true))
{
cellStyle = mxUtils.setStyle(cellStyle,
mxConstants.STYLE_STARTSIZE, size.height + 8);
if (collapsed)
{
geo.height = size.height + 8;
}
geo.width = size.width;
}
else
{
cellStyle = mxUtils.setStyle(cellStyle,
mxConstants.STYLE_STARTSIZE, size.width + 8);
if (collapsed)
{
geo.width = size.width + 8;
}
geo.height = size.height;
}
this.model.setStyle(cell, cellStyle);
}
else
{
var state = this.view.getState(cell) || this.view.createState(cell);
var align = (state.style[mxConstants.STYLE_ALIGN] || mxConstants.ALIGN_CENTER);
if (align == mxConstants.ALIGN_RIGHT)
{
geo.x += geo.width - size.width;
}
else if (align == mxConstants.ALIGN_CENTER)
{
geo.x += Math.round((geo.width - size.width) / 2);
}
var valign = this.getVerticalAlign(state);
if (valign == mxConstants.ALIGN_BOTTOM)
{
geo.y += geo.height - size.height;
}
else if (valign == mxConstants.ALIGN_MIDDLE)
{
geo.y += Math.round((geo.height - size.height) / 2);
}
geo.width = size.width;
geo.height = size.height;
}
if (!ignoreChildren && !collapsed)
{
var bounds = this.view.getBounds(this.model.getChildren(cell));
if (bounds != null)
{
var tr = this.view.translate;
var scale = this.view.scale;
var width = (bounds.x + bounds.width) / scale - geo.x - tr.x;
var height = (bounds.y + bounds.height) / scale - geo.y - tr.y;
geo.width = Math.max(geo.width, width);
geo.height = Math.max(geo.height, height);
}
}
this.cellsResized([cell], [geo], false);
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: getPreferredSizeForCell
*
* Returns the preferred width and height of the given <mxCell> as an
* <mxRectangle>. To implement a minimum width, add a new style eg.
* minWidth in the vertex and override this method as follows.
*
* (code)
* var graphGetPreferredSizeForCell = graph.getPreferredSizeForCell;
* graph.getPreferredSizeForCell = function(cell)
* {
* var result = graphGetPreferredSizeForCell.apply(this, arguments);
* var style = this.getCellStyle(cell);
*
* if (style['minWidth'] > 0)
* {
* result.width = Math.max(style['minWidth'], result.width);
* }
*
* return result;
* };
* (end)
*
* Parameters:
*
* cell - <mxCell> for which the preferred size should be returned.
*/
mxGraph.prototype.getPreferredSizeForCell = function(cell)
{
var result = null;
if (cell != null)
{
var state = this.view.getState(cell) || this.view.createState(cell);
var style = state.style;
if (!this.model.isEdge(cell))
{
var fontSize = style[mxConstants.STYLE_FONTSIZE] || mxConstants.DEFAULT_FONTSIZE;
var dx = 0;
var dy = 0;
// Adds dimension of image if shape is a label
if (this.getImage(state) != null || style[mxConstants.STYLE_IMAGE] != null)
{
if (style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_LABEL)
{
if (style[mxConstants.STYLE_VERTICAL_ALIGN] == mxConstants.ALIGN_MIDDLE)
{
dx += parseFloat(style[mxConstants.STYLE_IMAGE_WIDTH]) || mxLabel.prototype.imageSize;
}
if (style[mxConstants.STYLE_ALIGN] != mxConstants.ALIGN_CENTER)
{
dy += parseFloat(style[mxConstants.STYLE_IMAGE_HEIGHT]) || mxLabel.prototype.imageSize;
}
}
}
// Adds spacings
dx += 2 * (style[mxConstants.STYLE_SPACING] || 0);
dx += style[mxConstants.STYLE_SPACING_LEFT] || 0;
dx += style[mxConstants.STYLE_SPACING_RIGHT] || 0;
dy += 2 * (style[mxConstants.STYLE_SPACING] || 0);
dy += style[mxConstants.STYLE_SPACING_TOP] || 0;
dy += style[mxConstants.STYLE_SPACING_BOTTOM] || 0;
// Add spacing for collapse/expand icon
// LATER: Check alignment and use constants
// for image spacing
var image = this.getFoldingImage(state);
if (image != null)
{
dx += image.width + 8;
}
// Adds space for label
var value = this.cellRenderer.getLabelValue(state);
if (value != null && value.length > 0)
{
if (!this.isHtmlLabel(state.cell))
{
value = mxUtils.htmlEntities(value);
}
value = value.replace(/\n/g, '<br>');
var size = mxUtils.getSizeForString(value, fontSize, style[mxConstants.STYLE_FONTFAMILY]);
var width = size.width + dx;
var height = size.height + dy;
if (!mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true))
{
var tmp = height;
height = width;
width = tmp;
}
if (this.gridEnabled)
{
width = this.snap(width + this.gridSize / 2);
height = this.snap(height + this.gridSize / 2);
}
result = new mxRectangle(0, 0, width, height);
}
else
{
var gs2 = 4 * this.gridSize;
result = new mxRectangle(0, 0, gs2, gs2);
}
}
}
return result;
};
/**
* Function: resizeCell
*
* Sets the bounds of the given cell using <resizeCells>. Returns the
* cell which was passed to the function.
*
* Parameters:
*
* cell - <mxCell> whose bounds should be changed.
* bounds - <mxRectangle> that represents the new bounds.
*/
mxGraph.prototype.resizeCell = function(cell, bounds, recurse)
{
return this.resizeCells([cell], [bounds], recurse)[0];
};
/**
* Function: resizeCells
*
* Sets the bounds of the given cells and fires a <mxEvent.RESIZE_CELLS>
* event while the transaction is in progress. Returns the cells which
* have been passed to the function.
*
* Parameters:
*
* cells - Array of <mxCells> whose bounds should be changed.
* bounds - Array of <mxRectangles> that represent the new bounds.
*/
mxGraph.prototype.resizeCells = function(cells, bounds, recurse)
{
recurse = (recurse != null) ? recurse : this.isRecursiveResize();
this.model.beginUpdate();
try
{
this.cellsResized(cells, bounds, recurse);
this.fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS,
'cells', cells, 'bounds', bounds));
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: cellsResized
*
* Sets the bounds of the given cells and fires a <mxEvent.CELLS_RESIZED>
* event. If <extendParents> is true, then the parent is extended if a
* child size is changed so that it overlaps with the parent.
*
* The following example shows how to control group resizes to make sure
* that all child cells stay within the group.
*
* (code)
* graph.addListener(mxEvent.CELLS_RESIZED, function(sender, evt)
* {
* var cells = evt.getProperty('cells');
*
* if (cells != null)
* {
* for (var i = 0; i < cells.length; i++)
* {
* if (graph.getModel().getChildCount(cells[i]) > 0)
* {
* var geo = graph.getCellGeometry(cells[i]);
*
* if (geo != null)
* {
* var children = graph.getChildCells(cells[i], true, true);
* var bounds = graph.getBoundingBoxFromGeometry(children, true);
*
* geo = geo.clone();
* geo.width = Math.max(geo.width, bounds.width);
* geo.height = Math.max(geo.height, bounds.height);
*
* graph.getModel().setGeometry(cells[i], geo);
* }
* }
* }
* }
* });
* (end)
*
* Parameters:
*
* cells - Array of <mxCells> whose bounds should be changed.
* bounds - Array of <mxRectangles> that represent the new bounds.
* recurse - Optional boolean that specifies if the children should be resized.
*/
mxGraph.prototype.cellsResized = function(cells, bounds, recurse)
{
recurse = (recurse != null) ? recurse : false;
if (cells != null && bounds != null && cells.length == bounds.length)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
this.cellResized(cells[i], bounds[i], false, recurse);
if (this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
this.constrainChild(cells[i]);
}
if (this.resetEdgesOnResize)
{
this.resetEdges(cells);
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_RESIZED,
'cells', cells, 'bounds', bounds));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: cellResized
*
* Resizes the parents recursively so that they contain the complete area
* of the resized child cell.
*
* Parameters:
*
* cell - <mxCell> whose bounds should be changed.
* bounds - <mxRectangles> that represent the new bounds.
* ignoreRelative - Boolean that indicates if relative cells should be ignored.
* recurse - Optional boolean that specifies if the children should be resized.
*/
mxGraph.prototype.cellResized = function(cell, bounds, ignoreRelative, recurse)
{
var geo = this.model.getGeometry(cell);
if (geo != null && (geo.x != bounds.x || geo.y != bounds.y ||
geo.width != bounds.width || geo.height != bounds.height))
{
geo = geo.clone();
if (!ignoreRelative && geo.relative)
{
var offset = geo.offset;
if (offset != null)
{
offset.x += bounds.x - geo.x;
offset.y += bounds.y - geo.y;
}
}
else
{
geo.x = bounds.x;
geo.y = bounds.y;
}
geo.width = bounds.width;
geo.height = bounds.height;
if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates())
{
geo.x = Math.max(0, geo.x);
geo.y = Math.max(0, geo.y);
}
this.model.beginUpdate();
try
{
if (recurse)
{
this.resizeChildCells(cell, geo);
}
this.model.setGeometry(cell, geo);
this.constrainChildCells(cell);
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: resizeChildCells
*
* Resizes the child cells of the given cell for the given new geometry with
* respect to the current geometry of the cell.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
* newGeo - <mxGeometry> that represents the new bounds.
*/
mxGraph.prototype.resizeChildCells = function(cell, newGeo)
{
var geo = this.model.getGeometry(cell);
var dx = newGeo.width / geo.width;
var dy = newGeo.height / geo.height;
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.scaleCell(this.model.getChildAt(cell, i), dx, dy, true);
}
};
/**
* Function: constrainChildCells
*
* Constrains the children of the given cell using <constrainChild>.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
*/
mxGraph.prototype.constrainChildCells = function(cell)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.constrainChild(this.model.getChildAt(cell, i));
}
};
/**
* Function: scaleCell
*
* Scales the points, position and size of the given cell according to the
* given vertical and horizontal scaling factors.
*
* Parameters:
*
* cell - <mxCell> whose geometry should be scaled.
* dx - Horizontal scaling factor.
* dy - Vertical scaling factor.
* recurse - Boolean indicating if the child cells should be scaled.
*/
mxGraph.prototype.scaleCell = function(cell, dx, dy, recurse)
{
var geo = this.model.getGeometry(cell);
if (geo != null)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
geo = geo.clone();
// Stores values for restoring based on style
var x = geo.x;
var y = geo.y
var w = geo.width;
var h = geo.height;
geo.scale(dx, dy, style[mxConstants.STYLE_ASPECT] == 'fixed');
if (style[mxConstants.STYLE_RESIZE_WIDTH] == '1')
{
geo.width = w * dx;
}
else if (style[mxConstants.STYLE_RESIZE_WIDTH] == '0')
{
geo.width = w;
}
if (style[mxConstants.STYLE_RESIZE_HEIGHT] == '1')
{
geo.height = h * dy;
}
else if (style[mxConstants.STYLE_RESIZE_HEIGHT] == '0')
{
geo.height = h;
}
if (!this.isCellMovable(cell))
{
geo.x = x;
geo.y = y;
}
if (!this.isCellResizable(cell))
{
geo.width = w;
geo.height = h;
}
if (this.model.isVertex(cell))
{
this.cellResized(cell, geo, true, recurse);
}
else
{
this.model.setGeometry(cell, geo);
}
}
};
/**
* Function: extendParent
*
* Resizes the parents recursively so that they contain the complete area
* of the resized child cell.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
*/
mxGraph.prototype.extendParent = function(cell)
{
if (cell != null)
{
var parent = this.model.getParent(cell);
var p = this.getCellGeometry(parent);
if (parent != null && p != null && !this.isCellCollapsed(parent))
{
var geo = this.getCellGeometry(cell);
if (geo != null && !geo.relative &&
(p.width < geo.x + geo.width ||
p.height < geo.y + geo.height))
{
p = p.clone();
p.width = Math.max(p.width, geo.x + geo.width);
p.height = Math.max(p.height, geo.y + geo.height);
this.cellsResized([parent], [p], false);
}
}
}
};
/**
* Group: Cell moving
*/
/**
* Function: importCells
*
* Clones and inserts the given cells into the graph using the move
* method and returns the inserted cells. This shortcut is used if
* cells are inserted via datatransfer.
*
* Parameters:
*
* cells - Array of <mxCells> to be imported.
* dx - Integer that specifies the x-coordinate of the vector. Default is 0.
* dy - Integer that specifies the y-coordinate of the vector. Default is 0.
* target - <mxCell> that represents the new parent of the cells.
* evt - Mouseevent that triggered the invocation.
* mapping - Optional mapping for existing clones.
*/
mxGraph.prototype.importCells = function(cells, dx, dy, target, evt, mapping)
{
return this.moveCells(cells, dx, dy, true, target, evt, mapping);
};
/**
* Function: moveCells
*
* Moves or clones the specified cells and moves the cells or clones by the
* given amount, adding them to the optional target cell. The evt is the
* mouse event as the mouse was released. The change is carried out using
* <cellsMoved>. This method fires <mxEvent.MOVE_CELLS> while the
* transaction is in progress. Returns the cells that were moved.
*
* Use the following code to move all cells in the graph.
*
* (code)
* graph.moveCells(graph.getChildCells(null, true, true), 10, 10);
* (end)
*
* Parameters:
*
* cells - Array of <mxCells> to be moved, cloned or added to the target.
* dx - Integer that specifies the x-coordinate of the vector. Default is 0.
* dy - Integer that specifies the y-coordinate of the vector. Default is 0.
* clone - Boolean indicating if the cells should be cloned. Default is false.
* target - <mxCell> that represents the new parent of the cells.
* evt - Mouseevent that triggered the invocation.
* mapping - Optional mapping for existing clones.
*/
mxGraph.prototype.moveCells = function(cells, dx, dy, clone, target, evt, mapping)
{
dx = (dx != null) ? dx : 0;
dy = (dy != null) ? dy : 0;
clone = (clone != null) ? clone : false;
if (cells != null && (dx != 0 || dy != 0 || clone || target != null))
{
// Removes descendants with ancestors in cells to avoid multiple moving
cells = this.model.getTopmostCells(cells);
this.model.beginUpdate();
try
{
// Faster cell lookups to remove relative edge labels with selected
// terminals to avoid explicit and implicit move at same time
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
var isSelected = mxUtils.bind(this, function(cell)
{
while (cell != null)
{
if (dict.get(cell))
{
return true;
}
cell = this.model.getParent(cell);
}
return false;
});
// Removes relative edge labels with selected terminals
var checked = [];
for (var i = 0; i < cells.length; i++)
{
var geo = this.getCellGeometry(cells[i]);
var parent = this.model.getParent(cells[i]);
if ((geo == null || !geo.relative) || !this.model.isEdge(parent) ||
(!isSelected(this.model.getTerminal(parent, true)) &&
!isSelected(this.model.getTerminal(parent, false))))
{
checked.push(cells[i]);
}
}
cells = checked;
if (clone)
{
cells = this.cloneCells(cells, this.isCloneInvalidEdges(), mapping);
if (target == null)
{
target = this.getDefaultParent();
}
}
// FIXME: Cells should always be inserted first before any other edit
// to avoid forward references in sessions.
// Need to disable allowNegativeCoordinates if target not null to
// allow for temporary negative numbers until cellsAdded is called.
var previous = this.isAllowNegativeCoordinates();
if (target != null)
{
this.setAllowNegativeCoordinates(true);
}
this.cellsMoved(cells, dx, dy, !clone && this.isDisconnectOnMove()
&& this.isAllowDanglingEdges(), target == null,
this.isExtendParentsOnMove() && target == null);
this.setAllowNegativeCoordinates(previous);
if (target != null)
{
var index = this.model.getChildCount(target);
this.cellsAdded(cells, target, index, null, null, true);
}
// Dispatches a move event
this.fireEvent(new mxEventObject(mxEvent.MOVE_CELLS, 'cells', cells,
'dx', dx, 'dy', dy, 'clone', clone, 'target', target, 'event', evt));
}
finally
{
this.model.endUpdate();
}
}
return cells;
};
/**
* Function: cellsMoved
*
* Moves the specified cells by the given vector, disconnecting the cells
* using disconnectGraph is disconnect is true. This method fires
* <mxEvent.CELLS_MOVED> while the transaction is in progress.
*/
mxGraph.prototype.cellsMoved = function(cells, dx, dy, disconnect, constrain, extend)
{
if (cells != null && (dx != 0 || dy != 0))
{
extend = (extend != null) ? extend : false;
this.model.beginUpdate();
try
{
if (disconnect)
{
this.disconnectGraph(cells);
}
for (var i = 0; i < cells.length; i++)
{
this.translateCell(cells[i], dx, dy);
if (extend && this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
else if (constrain)
{
this.constrainChild(cells[i]);
}
}
if (this.resetEdgesOnMove)
{
this.resetEdges(cells);
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_MOVED,
'cells', cells, 'dx', dx, 'dy', dy, 'disconnect', disconnect));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: translateCell
*
* Translates the geometry of the given cell and stores the new,
* translated geometry in the model as an atomic change.
*/
mxGraph.prototype.translateCell = function(cell, dx, dy)
{
var geo = this.model.getGeometry(cell);
if (geo != null)
{
dx = parseFloat(dx);
dy = parseFloat(dy);
geo = geo.clone();
geo.translate(dx, dy);
if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates())
{
geo.x = Math.max(0, parseFloat(geo.x));
geo.y = Math.max(0, parseFloat(geo.y));
}
if (geo.relative && !this.model.isEdge(cell))
{
var parent = this.model.getParent(cell);
var angle = 0;
if (this.model.isVertex(parent))
{
var state = this.view.getState(parent);
var style = (state != null) ? state.style : this.getCellStyle(parent);
angle = mxUtils.getValue(style, mxConstants.STYLE_ROTATION, 0);
}
if (angle != 0)
{
var rad = mxUtils.toRadians(-angle);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var pt = mxUtils.getRotatedPoint(new mxPoint(dx, dy), cos, sin, new mxPoint(0, 0));
dx = pt.x;
dy = pt.y;
}
if (geo.offset == null)
{
geo.offset = new mxPoint(dx, dy);
}
else
{
geo.offset.x = parseFloat(geo.offset.x) + dx;
geo.offset.y = parseFloat(geo.offset.y) + dy;
}
}
this.model.setGeometry(cell, geo);
}
};
/**
* Function: getCellContainmentArea
*
* Returns the <mxRectangle> inside which a cell is to be kept.
*
* Parameters:
*
* cell - <mxCell> for which the area should be returned.
*/
mxGraph.prototype.getCellContainmentArea = function(cell)
{
if (cell != null && !this.model.isEdge(cell))
{
var parent = this.model.getParent(cell);
if (parent != null && parent != this.getDefaultParent())
{
var g = this.model.getGeometry(parent);
if (g != null)
{
var x = 0;
var y = 0;
var w = g.width;
var h = g.height;
if (this.isSwimlane(parent))
{
var size = this.getStartSize(parent);
var state = this.view.getState(parent);
var style = (state != null) ? state.style : this.getCellStyle(parent);
var dir = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST);
var flipH = mxUtils.getValue(style, mxConstants.STYLE_FLIPH, 0) == 1;
var flipV = mxUtils.getValue(style, mxConstants.STYLE_FLIPV, 0) == 1;
if (dir == mxConstants.DIRECTION_SOUTH || dir == mxConstants.DIRECTION_NORTH)
{
var tmp = size.width;
size.width = size.height;
size.height = tmp;
}
if ((dir == mxConstants.DIRECTION_EAST && !flipV) || (dir == mxConstants.DIRECTION_NORTH && !flipH) ||
(dir == mxConstants.DIRECTION_WEST && flipV) || (dir == mxConstants.DIRECTION_SOUTH && flipH))
{
x = size.width;
y = size.height;
}
w -= size.width;
h -= size.height;
}
return new mxRectangle(x, y, w, h);
}
}
}
return null;
};
/**
* Function: getMaximumGraphBounds
*
* Returns the bounds inside which the diagram should be kept as an
* <mxRectangle>.
*/
mxGraph.prototype.getMaximumGraphBounds = function()
{
return this.maximumGraphBounds;
};
/**
* Function: constrainChild
*
* Keeps the given cell inside the bounds returned by
* <getCellContainmentArea> for its parent, according to the rules defined by
* <getOverlap> and <isConstrainChild>. This modifies the cell's geometry
* in-place and does not clone it.
*
* Parameters:
*
* cells - <mxCell> which should be constrained.
* sizeFirst - Specifies if the size should be changed first. Default is true.
*/
mxGraph.prototype.constrainChild = function(cell, sizeFirst)
{
sizeFirst = (sizeFirst != null) ? sizeFirst : true;
if (cell != null)
{
var geo = this.getCellGeometry(cell);
if (geo != null && (this.isConstrainRelativeChildren() || !geo.relative))
{
var parent = this.model.getParent(cell);
var pgeo = this.getCellGeometry(parent);
var max = this.getMaximumGraphBounds();
// Finds parent offset
if (max != null)
{
var off = this.getBoundingBoxFromGeometry([parent], false);
if (off != null)
{
max = mxRectangle.fromRectangle(max);
max.x -= off.x;
max.y -= off.y;
}
}
if (this.isConstrainChild(cell))
{
var tmp = this.getCellContainmentArea(cell);
if (tmp != null)
{
var overlap = this.getOverlap(cell);
if (overlap > 0)
{
tmp = mxRectangle.fromRectangle(tmp);
tmp.x -= tmp.width * overlap;
tmp.y -= tmp.height * overlap;
tmp.width += 2 * tmp.width * overlap;
tmp.height += 2 * tmp.height * overlap;
}
// Find the intersection between max and tmp
if (max == null)
{
max = tmp;
}
else
{
max = mxRectangle.fromRectangle(max);
max.intersect(tmp);
}
}
}
if (max != null)
{
var cells = [cell];
if (!this.isCellCollapsed(cell))
{
var desc = this.model.getDescendants(cell);
for (var i = 0; i < desc.length; i++)
{
if (this.isCellVisible(desc[i]))
{
cells.push(desc[i]);
}
}
}
var bbox = this.getBoundingBoxFromGeometry(cells, false);
if (bbox != null)
{
geo = geo.clone();
// Cumulative horizontal movement
var dx = 0;
if (geo.width > max.width)
{
dx = geo.width - max.width;
geo.width -= dx;
}
if (bbox.x + bbox.width > max.x + max.width)
{
dx -= bbox.x + bbox.width - max.x - max.width - dx;
}
// Cumulative vertical movement
var dy = 0;
if (geo.height > max.height)
{
dy = geo.height - max.height;
geo.height -= dy;
}
if (bbox.y + bbox.height > max.y + max.height)
{
dy -= bbox.y + bbox.height - max.y - max.height - dy;
}
if (bbox.x < max.x)
{
dx -= bbox.x - max.x;
}
if (bbox.y < max.y)
{
dy -= bbox.y - max.y;
}
if (dx != 0 || dy != 0)
{
if (geo.relative)
{
// Relative geometries are moved via absolute offset
if (geo.offset == null)
{
geo.offset = new mxPoint();
}
geo.offset.x += dx;
geo.offset.y += dy;
}
else
{
geo.x += dx;
geo.y += dy;
}
}
this.model.setGeometry(cell, geo);
}
}
}
}
};
/**
* Function: resetEdges
*
* Resets the control points of the edges that are connected to the given
* cells if not both ends of the edge are in the given cells array.
*
* Parameters:
*
* cells - Array of <mxCells> for which the connected edges should be
* reset.
*/
mxGraph.prototype.resetEdges = function(cells)
{
if (cells != null)
{
// Prepares faster cells lookup
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
var edges = this.model.getEdges(cells[i]);
if (edges != null)
{
for (var j = 0; j < edges.length; j++)
{
var state = this.view.getState(edges[j]);
var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[j], true);
var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[j], false);
// Checks if one of the terminals is not in the given array
if (!dict.get(source) || !dict.get(target))
{
this.resetEdge(edges[j]);
}
}
}
this.resetEdges(this.model.getChildren(cells[i]));
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: resetEdge
*
* Resets the control points of the given edge.
*
* Parameters:
*
* edge - <mxCell> whose points should be reset.
*/
mxGraph.prototype.resetEdge = function(edge)
{
var geo = this.model.getGeometry(edge);
// Resets the control points
if (geo != null && geo.points != null && geo.points.length > 0)
{
geo = geo.clone();
geo.points = [];
this.model.setGeometry(edge, geo);
}
return edge;
};
/**
* Group: Cell connecting and connection constraints
*/
/**
* Function: getOutlineConstraint
*
* Returns the constraint used to connect to the outline of the given state.
*/
mxGraph.prototype.getOutlineConstraint = function(point, terminalState, me)
{
if (terminalState.shape != null)
{
var bounds = this.view.getPerimeterBounds(terminalState);
var direction = terminalState.style[mxConstants.STYLE_DIRECTION];
if (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH)
{
bounds.x += bounds.width / 2 - bounds.height / 2;
bounds.y += bounds.height / 2 - bounds.width / 2;
var tmp = bounds.width;
bounds.width = bounds.height;
bounds.height = tmp;
}
var alpha = mxUtils.toRadians(terminalState.shape.getShapeRotation());
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var ct = new mxPoint(bounds.getCenterX(), bounds.getCenterY());
point = mxUtils.getRotatedPoint(point, cos, sin, ct);
}
var sx = 1;
var sy = 1;
var dx = 0;
var dy = 0;
// LATER: Add flipping support for image shapes
if (this.getModel().isVertex(terminalState.cell))
{
var flipH = terminalState.style[mxConstants.STYLE_FLIPH];
var flipV = terminalState.style[mxConstants.STYLE_FLIPV];
// Legacy support for stencilFlipH/V
if (terminalState.shape != null && terminalState.shape.stencil != null)
{
flipH = mxUtils.getValue(terminalState.style, 'stencilFlipH', 0) == 1 || flipH;
flipV = mxUtils.getValue(terminalState.style, 'stencilFlipV', 0) == 1 || flipV;
}
if (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH)
{
var tmp = flipH;
flipH = flipV;
flipV = tmp;
}
if (flipH)
{
sx = -1;
dx = -bounds.width;
}
if (flipV)
{
sy = -1;
dy = -bounds.height ;
}
}
point = new mxPoint((point.x - bounds.x) * sx - dx + bounds.x, (point.y - bounds.y) * sy - dy + bounds.y);
var x = (bounds.width == 0) ? 0 : Math.round((point.x - bounds.x) * 1000 / bounds.width) / 1000;
var y = (bounds.height == 0) ? 0 : Math.round((point.y - bounds.y) * 1000 / bounds.height) / 1000;
return new mxConnectionConstraint(new mxPoint(x, y), false);
}
return null;
};
/**
* Function: getAllConnectionConstraints
*
* Returns an array of all <mxConnectionConstraints> for the given terminal. If
* the shape of the given terminal is a <mxStencilShape> then the constraints
* of the corresponding <mxStencil> are returned.
*
* Parameters:
*
* terminal - <mxCellState> that represents the terminal.
* source - Boolean that specifies if the terminal is the source or target.
*/
mxGraph.prototype.getAllConnectionConstraints = function(terminal, source)
{
if (terminal != null && terminal.shape != null && terminal.shape.stencil != null)
{
return terminal.shape.stencil.constraints;
}
return null;
};
/**
* Function: getConnectionConstraint
*
* Returns an <mxConnectionConstraint> that describes the given connection
* point. This result can then be passed to <getConnectionPoint>.
*
* Parameters:
*
* edge - <mxCellState> that represents the edge.
* terminal - <mxCellState> that represents the terminal.
* source - Boolean indicating if the terminal is the source or target.
*/
mxGraph.prototype.getConnectionConstraint = function(edge, terminal, source)
{
var point = null;
var x = edge.style[(source) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X];
if (x != null)
{
var y = edge.style[(source) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y];
if (y != null)
{
point = new mxPoint(parseFloat(x), parseFloat(y));
}
}
var perimeter = false;
var dx = 0, dy = 0;
if (point != null)
{
perimeter = mxUtils.getValue(edge.style, (source) ? mxConstants.STYLE_EXIT_PERIMETER :
mxConstants.STYLE_ENTRY_PERIMETER, true);
//Add entry/exit offset
dx = parseFloat(edge.style[(source) ? mxConstants.STYLE_EXIT_DX : mxConstants.STYLE_ENTRY_DX]);
dy = parseFloat(edge.style[(source) ? mxConstants.STYLE_EXIT_DY : mxConstants.STYLE_ENTRY_DY]);
dx = isFinite(dx)? dx : 0;
dy = isFinite(dy)? dy : 0;
}
return new mxConnectionConstraint(point, perimeter, null, dx, dy);
};
/**
* Function: setConnectionConstraint
*
* Sets the <mxConnectionConstraint> that describes the given connection point.
* If no constraint is given then nothing is changed. To remove an existing
* constraint from the given edge, use an empty constraint instead.
*
* Parameters:
*
* edge - <mxCell> that represents the edge.
* terminal - <mxCell> that represents the terminal.
* source - Boolean indicating if the terminal is the source or target.
* constraint - Optional <mxConnectionConstraint> to be used for this
* connection.
*/
mxGraph.prototype.setConnectionConstraint = function(edge, terminal, source, constraint)
{
if (constraint != null)
{
this.model.beginUpdate();
try
{
if (constraint == null || constraint.point == null)
{
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X :
mxConstants.STYLE_ENTRY_X, null, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y :
mxConstants.STYLE_ENTRY_Y, null, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_DX :
mxConstants.STYLE_ENTRY_DX, null, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_DY :
mxConstants.STYLE_ENTRY_DY, null, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER :
mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]);
}
else if (constraint.point != null)
{
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X :
mxConstants.STYLE_ENTRY_X, constraint.point.x, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y :
mxConstants.STYLE_ENTRY_Y, constraint.point.y, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_DX :
mxConstants.STYLE_ENTRY_DX, constraint.dx, [edge]);
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_DY :
mxConstants.STYLE_ENTRY_DY, constraint.dy, [edge]);
// Only writes 0 since 1 is default
if (!constraint.perimeter)
{
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER :
mxConstants.STYLE_ENTRY_PERIMETER, '0', [edge]);
}
else
{
this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER :
mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]);
}
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: getConnectionPoint
*
* Returns the nearest point in the list of absolute points or the center
* of the opposite terminal.
*
* Parameters:
*
* vertex - <mxCellState> that represents the vertex.
* constraint - <mxConnectionConstraint> that represents the connection point
* constraint as returned by <getConnectionConstraint>.
*/
mxGraph.prototype.getConnectionPoint = function(vertex, constraint, round)
{
round = (round != null) ? round : true;
var point = null;
if (vertex != null && constraint.point != null)
{
var bounds = this.view.getPerimeterBounds(vertex);
var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY());
var direction = vertex.style[mxConstants.STYLE_DIRECTION];
var r1 = 0;
// Bounds need to be rotated by 90 degrees for further computation
if (direction != null && mxUtils.getValue(vertex.style,
mxConstants.STYLE_ANCHOR_POINT_DIRECTION, 1) == 1)
{
if (direction == mxConstants.DIRECTION_NORTH)
{
r1 += 270;
}
else if (direction == mxConstants.DIRECTION_WEST)
{
r1 += 180;
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
r1 += 90;
}
// Bounds need to be rotated by 90 degrees for further computation
if (direction == mxConstants.DIRECTION_NORTH ||
direction == mxConstants.DIRECTION_SOUTH)
{
bounds.rotate90();
}
}
var scale = this.view.scale;
point = new mxPoint(bounds.x + constraint.point.x * bounds.width + constraint.dx * scale,
bounds.y + constraint.point.y * bounds.height + constraint.dy * scale);
// Rotation for direction before projection on perimeter
var r2 = vertex.style[mxConstants.STYLE_ROTATION] || 0;
if (constraint.perimeter)
{
if (r1 != 0)
{
// Only 90 degrees steps possible here so no trig needed
var cos = 0;
var sin = 0;
if (r1 == 90)
{
sin = 1;
}
else if (r1 == 180)
{
cos = -1;
}
else if (r1 == 270)
{
sin = -1;
}
point = mxUtils.getRotatedPoint(point, cos, sin, cx);
}
point = this.view.getPerimeterPoint(vertex, point, false);
}
else
{
r2 += r1;
if (this.getModel().isVertex(vertex.cell))
{
var flipH = vertex.style[mxConstants.STYLE_FLIPH] == 1;
var flipV = vertex.style[mxConstants.STYLE_FLIPV] == 1;
// Legacy support for stencilFlipH/V
if (vertex.shape != null && vertex.shape.stencil != null)
{
flipH = (mxUtils.getValue(vertex.style, 'stencilFlipH', 0) == 1) || flipH;
flipV = (mxUtils.getValue(vertex.style, 'stencilFlipV', 0) == 1) || flipV;
}
if (direction == mxConstants.DIRECTION_NORTH ||
direction == mxConstants.DIRECTION_SOUTH)
{
var temp = flipH;
flipH = flipV
flipV = temp;
}
if (flipH)
{
point.x = 2 * bounds.getCenterX() - point.x;
}
if (flipV)
{
point.y = 2 * bounds.getCenterY() - point.y;
}
}
}
// Generic rotation after projection on perimeter
if (r2 != 0 && point != null)
{
var rad = mxUtils.toRadians(r2);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
point = mxUtils.getRotatedPoint(point, cos, sin, cx);
}
}
if (round && point != null)
{
point.x = Math.round(point.x);
point.y = Math.round(point.y);
}
return point;
};
/**
* Function: connectCell
*
* Connects the specified end of the given edge to the given terminal
* using <cellConnected> and fires <mxEvent.CONNECT_CELL> while the
* transaction is in progress. Returns the updated edge.
*
* Parameters:
*
* edge - <mxCell> whose terminal should be updated.
* terminal - <mxCell> that represents the new terminal to be used.
* source - Boolean indicating if the new terminal is the source or target.
* constraint - Optional <mxConnectionConstraint> to be used for this
* connection.
*/
mxGraph.prototype.connectCell = function(edge, terminal, source, constraint)
{
this.model.beginUpdate();
try
{
var previous = this.model.getTerminal(edge, source);
this.cellConnected(edge, terminal, source, constraint);
this.fireEvent(new mxEventObject(mxEvent.CONNECT_CELL,
'edge', edge, 'terminal', terminal, 'source', source,
'previous', previous));
}
finally
{
this.model.endUpdate();
}
return edge;
};
/**
* Function: cellConnected
*
* Sets the new terminal for the given edge and resets the edge points if
* <resetEdgesOnConnect> is true. This method fires
* <mxEvent.CELL_CONNECTED> while the transaction is in progress.
*
* Parameters:
*
* edge - <mxCell> whose terminal should be updated.
* terminal - <mxCell> that represents the new terminal to be used.
* source - Boolean indicating if the new terminal is the source or target.
* constraint - <mxConnectionConstraint> to be used for this connection.
*/
mxGraph.prototype.cellConnected = function(edge, terminal, source, constraint)
{
if (edge != null)
{
this.model.beginUpdate();
try
{
var previous = this.model.getTerminal(edge, source);
// Updates the constraint
this.setConnectionConstraint(edge, terminal, source, constraint);
// Checks if the new terminal is a port, uses the ID of the port in the
// style and the parent of the port as the actual terminal of the edge.
if (this.isPortsEnabled())
{
var id = null;
if (this.isPort(terminal))
{
id = terminal.getId();
terminal = this.getTerminalForPort(terminal, source);
}
// Sets or resets all previous information for connecting to a child port
var key = (source) ? mxConstants.STYLE_SOURCE_PORT :
mxConstants.STYLE_TARGET_PORT;
this.setCellStyles(key, id, [edge]);
}
this.model.setTerminal(edge, terminal, source);
if (this.resetEdgesOnConnect)
{
this.resetEdge(edge);
}
this.fireEvent(new mxEventObject(mxEvent.CELL_CONNECTED,
'edge', edge, 'terminal', terminal, 'source', source,
'previous', previous));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: disconnectGraph
*
* Disconnects the given edges from the terminals which are not in the
* given array.
*
* Parameters:
*
* cells - Array of <mxCells> to be disconnected.
*/
mxGraph.prototype.disconnectGraph = function(cells)
{
if (cells != null)
{
this.model.beginUpdate();
try
{
var scale = this.view.scale;
var tr = this.view.translate;
// Fast lookup for finding cells in array
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
for (var i = 0; i < cells.length; i++)
{
if (this.model.isEdge(cells[i]))
{
var geo = this.model.getGeometry(cells[i]);
if (geo != null)
{
var state = this.view.getState(cells[i]);
var pstate = this.view.getState(
this.model.getParent(cells[i]));
if (state != null &&
pstate != null)
{
geo = geo.clone();
var dx = -pstate.origin.x;
var dy = -pstate.origin.y;
var pts = state.absolutePoints;
var src = this.model.getTerminal(cells[i], true);
if (src != null && this.isCellDisconnectable(cells[i], src, true))
{
while (src != null && !dict.get(src))
{
src = this.model.getParent(src);
}
if (src == null)
{
geo.setTerminalPoint(
new mxPoint(pts[0].x / scale - tr.x + dx,
pts[0].y / scale - tr.y + dy), true);
this.model.setTerminal(cells[i], null, true);
}
}
var trg = this.model.getTerminal(cells[i], false);
if (trg != null && this.isCellDisconnectable(cells[i], trg, false))
{
while (trg != null && !dict.get(trg))
{
trg = this.model.getParent(trg);
}
if (trg == null)
{
var n = pts.length - 1;
geo.setTerminalPoint(
new mxPoint(pts[n].x / scale - tr.x + dx,
pts[n].y / scale - tr.y + dy), false);
this.model.setTerminal(cells[i], null, false);
}
}
this.model.setGeometry(cells[i], geo);
}
}
}
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Group: Drilldown
*/
/**
* Function: getCurrentRoot
*
* Returns the current root of the displayed cell hierarchy. This is a
* shortcut to <mxGraphView.currentRoot> in <view>.
*/
mxGraph.prototype.getCurrentRoot = function()
{
return this.view.currentRoot;
};
/**
* Function: getTranslateForRoot
*
* Returns the translation to be used if the given cell is the root cell as
* an <mxPoint>. This implementation returns null.
*
* Example:
*
* To keep the children at their absolute position while stepping into groups,
* this function can be overridden as follows.
*
* (code)
* var offset = new mxPoint(0, 0);
*
* while (cell != null)
* {
* var geo = this.model.getGeometry(cell);
*
* if (geo != null)
* {
* offset.x -= geo.x;
* offset.y -= geo.y;
* }
*
* cell = this.model.getParent(cell);
* }
*
* return offset;
* (end)
*
* Parameters:
*
* cell - <mxCell> that represents the root.
*/
mxGraph.prototype.getTranslateForRoot = function(cell)
{
return null;
};
/**
* Function: isPort
*
* Returns true if the given cell is a "port", that is, when connecting to
* it, the cell returned by getTerminalForPort should be used as the
* terminal and the port should be referenced by the ID in either the
* mxConstants.STYLE_SOURCE_PORT or the or the
* mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable.
* This implementation always returns false.
*
* A typical implementation is the following:
*
* (code)
* graph.isPort = function(cell)
* {
* var geo = this.getCellGeometry(cell);
*
* return (geo != null) ? geo.relative : false;
* };
* (end)
*
* Parameters:
*
* cell - <mxCell> that represents the port.
*/
mxGraph.prototype.isPort = function(cell)
{
return false;
};
/**
* Function: getTerminalForPort
*
* Returns the terminal to be used for a given port. This implementation
* always returns the parent cell.
*
* Parameters:
*
* cell - <mxCell> that represents the port.
* source - If the cell is the source or target port.
*/
mxGraph.prototype.getTerminalForPort = function(cell, source)
{
return this.model.getParent(cell);
};
/**
* Function: getChildOffsetForCell
*
* Returns the offset to be used for the cells inside the given cell. The
* root and layer cells may be identified using <mxGraphModel.isRoot> and
* <mxGraphModel.isLayer>. For all other current roots, the
* <mxGraphView.currentRoot> field points to the respective cell, so that
* the following holds: cell == this.view.currentRoot. This implementation
* returns null.
*
* Parameters:
*
* cell - <mxCell> whose offset should be returned.
*/
mxGraph.prototype.getChildOffsetForCell = function(cell)
{
return null;
};
/**
* Function: enterGroup
*
* Uses the given cell as the root of the displayed cell hierarchy. If no
* cell is specified then the selection cell is used. The cell is only used
* if <isValidRoot> returns true.
*
* Parameters:
*
* cell - Optional <mxCell> to be used as the new root. Default is the
* selection cell.
*/
mxGraph.prototype.enterGroup = function(cell)
{
cell = cell || this.getSelectionCell();
if (cell != null && this.isValidRoot(cell))
{
this.view.setCurrentRoot(cell);
this.clearSelection();
}
};
/**
* Function: exitGroup
*
* Changes the current root to the next valid root in the displayed cell
* hierarchy.
*/
mxGraph.prototype.exitGroup = function()
{
var root = this.model.getRoot();
var current = this.getCurrentRoot();
if (current != null)
{
var next = this.model.getParent(current);
// Finds the next valid root in the hierarchy
while (next != root && !this.isValidRoot(next) &&
this.model.getParent(next) != root)
{
next = this.model.getParent(next);
}
// Clears the current root if the new root is
// the model's root or one of the layers.
if (next == root || this.model.getParent(next) == root)
{
this.view.setCurrentRoot(null);
}
else
{
this.view.setCurrentRoot(next);
}
var state = this.view.getState(current);
// Selects the previous root in the graph
if (state != null)
{
this.setSelectionCell(current);
}
}
};
/**
* Function: home
*
* Uses the root of the model as the root of the displayed cell hierarchy
* and selects the previous root.
*/
mxGraph.prototype.home = function()
{
var current = this.getCurrentRoot();
if (current != null)
{
this.view.setCurrentRoot(null);
var state = this.view.getState(current);
if (state != null)
{
this.setSelectionCell(current);
}
}
};
/**
* Function: isValidRoot
*
* Returns true if the given cell is a valid root for the cell display
* hierarchy. This implementation returns true for all non-null values.
*
* Parameters:
*
* cell - <mxCell> which should be checked as a possible root.
*/
mxGraph.prototype.isValidRoot = function(cell)
{
return (cell != null);
};
/**
* Group: Graph display
*/
/**
* Function: getGraphBounds
*
* Returns the bounds of the visible graph. Shortcut to
* <mxGraphView.getGraphBounds>. See also: <getBoundingBoxFromGeometry>.
*/
mxGraph.prototype.getGraphBounds = function()
{
return this.view.getGraphBounds();
};
/**
* Function: getCellBounds
*
* Returns the scaled, translated bounds for the given cell. See
* <mxGraphView.getBounds> for arrays.
*
* Parameters:
*
* cell - <mxCell> whose bounds should be returned.
* includeEdge - Optional boolean that specifies if the bounds of
* the connected edges should be included. Default is false.
* includeDescendants - Optional boolean that specifies if the bounds
* of all descendants should be included. Default is false.
*/
mxGraph.prototype.getCellBounds = function(cell, includeEdges, includeDescendants)
{
var cells = [cell];
// Includes all connected edges
if (includeEdges)
{
cells = cells.concat(this.model.getEdges(cell));
}
var result = this.view.getBounds(cells);
// Recursively includes the bounds of the children
if (includeDescendants)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var tmp = this.getCellBounds(this.model.getChildAt(cell, i),
includeEdges, true);
if (result != null)
{
result.add(tmp);
}
else
{
result = tmp;
}
}
}
return result;
};
/**
* Function: getBoundingBoxFromGeometry
*
* Returns the bounding box for the geometries of the vertices in the
* given array of cells. This can be used to find the graph bounds during
* a layout operation (ie. before the last endUpdate) as follows:
*
* (code)
* var cells = graph.getChildCells(graph.getDefaultParent(), true, true);
* var bounds = graph.getBoundingBoxFromGeometry(cells, true);
* (end)
*
* This can then be used to move cells to the origin:
*
* (code)
* if (bounds.x < 0 || bounds.y < 0)
* {
* graph.moveCells(cells, -Math.min(bounds.x, 0), -Math.min(bounds.y, 0))
* }
* (end)
*
* Or to translate the graph view:
*
* (code)
* if (bounds.x < 0 || bounds.y < 0)
* {
* graph.view.setTranslate(-Math.min(bounds.x, 0), -Math.min(bounds.y, 0));
* }
* (end)
*
* Parameters:
*
* cells - Array of <mxCells> whose bounds should be returned.
* includeEdges - Specifies if edge bounds should be included by computing
* the bounding box for all points in geometry. Default is false.
*/
mxGraph.prototype.getBoundingBoxFromGeometry = function(cells, includeEdges)
{
includeEdges = (includeEdges != null) ? includeEdges : false;
var result = null;
if (cells != null)
{
for (var i = 0; i < cells.length; i++)
{
if (includeEdges || this.model.isVertex(cells[i]))
{
// Computes the bounding box for the points in the geometry
var geo = this.getCellGeometry(cells[i]);
if (geo != null)
{
var bbox = null;
if (this.model.isEdge(cells[i]))
{
var addPoint = function(pt)
{
if (pt != null)
{
if (tmp == null)
{
tmp = new mxRectangle(pt.x, pt.y, 0, 0);
}
else
{
tmp.add(new mxRectangle(pt.x, pt.y, 0, 0));
}
}
};
if (this.model.getTerminal(cells[i], true) == null)
{
addPoint(geo.getTerminalPoint(true));
}
if (this.model.getTerminal(cells[i], false) == null)
{
addPoint(geo.getTerminalPoint(false));
}
var pts = geo.points;
if (pts != null && pts.length > 0)
{
var tmp = new mxRectangle(pts[0].x, pts[0].y, 0, 0);
for (var j = 1; j < pts.length; j++)
{
addPoint(pts[j]);
}
}
bbox = tmp;
}
else
{
var parent = this.model.getParent(cells[i]);
if (geo.relative)
{
if (this.model.isVertex(parent) && parent != this.view.currentRoot)
{
var tmp = this.getBoundingBoxFromGeometry([parent], false);
if (tmp != null)
{
bbox = new mxRectangle(geo.x * tmp.width, geo.y * tmp.height, geo.width, geo.height);
if (mxUtils.indexOf(cells, parent) >= 0)
{
bbox.x += tmp.x;
bbox.y += tmp.y;
}
}
}
}
else
{
bbox = mxRectangle.fromRectangle(geo);
if (this.model.isVertex(parent) && mxUtils.indexOf(cells, parent) >= 0)
{
var tmp = this.getBoundingBoxFromGeometry([parent], false);
if (tmp != null)
{
bbox.x += tmp.x;
bbox.y += tmp.y;
}
}
}
if (bbox != null && geo.offset != null)
{
bbox.x += geo.offset.x;
bbox.y += geo.offset.y;
}
}
if (bbox != null)
{
if (result == null)
{
result = mxRectangle.fromRectangle(bbox);
}
else
{
result.add(bbox);
}
}
}
}
}
}
return result;
};
/**
* Function: refresh
*
* Clears all cell states or the states for the hierarchy starting at the
* given cell and validates the graph. This fires a refresh event as the
* last step.
*
* Parameters:
*
* cell - Optional <mxCell> for which the cell states should be cleared.
*/
mxGraph.prototype.refresh = function(cell)
{
this.view.clear(cell, cell == null);
this.view.validate();
this.sizeDidChange();
this.fireEvent(new mxEventObject(mxEvent.REFRESH));
};
/**
* Function: snap
*
* Snaps the given numeric value to the grid if <gridEnabled> is true.
*
* Parameters:
*
* value - Numeric value to be snapped to the grid.
*/
mxGraph.prototype.snap = function(value)
{
if (this.gridEnabled)
{
value = Math.round(value / this.gridSize ) * this.gridSize;
}
return value;
};
/**
* Function: panGraph
*
* Shifts the graph display by the given amount. This is used to preview
* panning operations, use <mxGraphView.setTranslate> to set a persistent
* translation of the view. Fires <mxEvent.PAN>.
*
* Parameters:
*
* dx - Amount to shift the graph along the x-axis.
* dy - Amount to shift the graph along the y-axis.
*/
mxGraph.prototype.panGraph = function(dx, dy)
{
if (this.useScrollbarsForPanning && mxUtils.hasScrollbars(this.container))
{
this.container.scrollLeft = -dx;
this.container.scrollTop = -dy;
}
else
{
var canvas = this.view.getCanvas();
if (this.dialect == mxConstants.DIALECT_SVG)
{
// Puts everything inside the container in a DIV so that it
// can be moved without changing the state of the container
if (dx == 0 && dy == 0)
{
// Workaround for ignored removeAttribute on SVG element in IE9 standards
if (mxClient.IS_IE)
{
canvas.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
}
else
{
canvas.removeAttribute('transform');
}
if (this.shiftPreview1 != null)
{
var child = this.shiftPreview1.firstChild;
while (child != null)
{
var next = child.nextSibling;
this.container.appendChild(child);
child = next;
}
if (this.shiftPreview1.parentNode != null)
{
this.shiftPreview1.parentNode.removeChild(this.shiftPreview1);
}
this.shiftPreview1 = null;
this.container.appendChild(canvas.parentNode);
child = this.shiftPreview2.firstChild;
while (child != null)
{
var next = child.nextSibling;
this.container.appendChild(child);
child = next;
}
if (this.shiftPreview2.parentNode != null)
{
this.shiftPreview2.parentNode.removeChild(this.shiftPreview2);
}
this.shiftPreview2 = null;
}
}
else
{
canvas.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
if (this.shiftPreview1 == null)
{
// Needs two divs for stuff before and after the SVG element
this.shiftPreview1 = document.createElement('div');
this.shiftPreview1.style.position = 'absolute';
this.shiftPreview1.style.overflow = 'visible';
this.shiftPreview2 = document.createElement('div');
this.shiftPreview2.style.position = 'absolute';
this.shiftPreview2.style.overflow = 'visible';
var current = this.shiftPreview1;
var child = this.container.firstChild;
while (child != null)
{
var next = child.nextSibling;
// SVG element is moved via transform attribute
if (child != canvas.parentNode)
{
current.appendChild(child);
}
else
{
current = this.shiftPreview2;
}
child = next;
}
// Inserts elements only if not empty
if (this.shiftPreview1.firstChild != null)
{
this.container.insertBefore(this.shiftPreview1, canvas.parentNode);
}
if (this.shiftPreview2.firstChild != null)
{
this.container.appendChild(this.shiftPreview2);
}
}
this.shiftPreview1.style.left = dx + 'px';
this.shiftPreview1.style.top = dy + 'px';
this.shiftPreview2.style.left = dx + 'px';
this.shiftPreview2.style.top = dy + 'px';
}
}
else
{
canvas.style.left = dx + 'px';
canvas.style.top = dy + 'px';
}
this.panDx = dx;
this.panDy = dy;
this.fireEvent(new mxEventObject(mxEvent.PAN));
}
};
/**
* Function: zoomIn
*
* Zooms into the graph by <zoomFactor>.
*/
mxGraph.prototype.zoomIn = function()
{
this.zoom(this.zoomFactor);
};
/**
* Function: zoomOut
*
* Zooms out of the graph by <zoomFactor>.
*/
mxGraph.prototype.zoomOut = function()
{
this.zoom(1 / this.zoomFactor);
};
/**
* Function: zoomActual
*
* Resets the zoom and panning in the view.
*/
mxGraph.prototype.zoomActual = function()
{
if (this.view.scale == 1)
{
this.view.setTranslate(0, 0);
}
else
{
this.view.translate.x = 0;
this.view.translate.y = 0;
this.view.setScale(1);
}
};
/**
* Function: zoomTo
*
* Zooms the graph to the given scale with an optional boolean center
* argument, which is passd to <zoom>.
*/
mxGraph.prototype.zoomTo = function(scale, center)
{
this.zoom(scale / this.view.scale, center);
};
/**
* Function: center
*
* Centers the graph in the container.
*
* Parameters:
*
* horizontal - Optional boolean that specifies if the graph should be centered
* horizontally. Default is true.
* vertical - Optional boolean that specifies if the graph should be centered
* vertically. Default is true.
* cx - Optional float that specifies the horizontal center. Default is 0.5.
* cy - Optional float that specifies the vertical center. Default is 0.5.
*/
mxGraph.prototype.center = function(horizontal, vertical, cx, cy)
{
horizontal = (horizontal != null) ? horizontal : true;
vertical = (vertical != null) ? vertical : true;
cx = (cx != null) ? cx : 0.5;
cy = (cy != null) ? cy : 0.5;
var hasScrollbars = mxUtils.hasScrollbars(this.container);
var padding = 2 * this.getBorder();
var cw = this.container.clientWidth - padding;
var ch = this.container.clientHeight - padding;
var bounds = this.getGraphBounds();
var t = this.view.translate;
var s = this.view.scale;
var dx = (horizontal) ? cw - bounds.width : 0;
var dy = (vertical) ? ch - bounds.height : 0;
if (!hasScrollbars)
{
this.view.setTranslate((horizontal) ? Math.floor(t.x - bounds.x * s + dx * cx / s) : t.x,
(vertical) ? Math.floor(t.y - bounds.y * s + dy * cy / s) : t.y);
}
else
{
bounds.x -= t.x;
bounds.y -= t.y;
var sw = this.container.scrollWidth;
var sh = this.container.scrollHeight;
if (sw > cw)
{
dx = 0;
}
if (sh > ch)
{
dy = 0;
}
this.view.setTranslate(Math.floor(dx / 2 - bounds.x), Math.floor(dy / 2 - bounds.y));
this.container.scrollLeft = (sw - cw) / 2;
this.container.scrollTop = (sh - ch) / 2;
}
};
/**
* Function: zoom
*
* Zooms the graph using the given factor. Center is an optional boolean
* argument that keeps the graph scrolled to the center. If the center argument
* is omitted, then <centerZoom> will be used as its value.
*/
mxGraph.prototype.zoom = function(factor, center)
{
center = (center != null) ? center : this.centerZoom;
var scale = Math.round(this.view.scale * factor * 100) / 100;
var state = this.view.getState(this.getSelectionCell());
factor = scale / this.view.scale;
if (this.keepSelectionVisibleOnZoom && state != null)
{
var rect = new mxRectangle(state.x * factor, state.y * factor,
state.width * factor, state.height * factor);
// Refreshes the display only once if a scroll is carried out
this.view.scale = scale;
if (!this.scrollRectToVisible(rect))
{
this.view.revalidate();
// Forces an event to be fired but does not revalidate again
this.view.setScale(scale);
}
}
else
{
var hasScrollbars = mxUtils.hasScrollbars(this.container);
if (center && !hasScrollbars)
{
var dx = this.container.offsetWidth;
var dy = this.container.offsetHeight;
if (factor > 1)
{
var f = (factor - 1) / (scale * 2);
dx *= -f;
dy *= -f;
}
else
{
var f = (1 / factor - 1) / (this.view.scale * 2);
dx *= f;
dy *= f;
}
this.view.scaleAndTranslate(scale,
this.view.translate.x + dx,
this.view.translate.y + dy);
}
else
{
// Allows for changes of translate and scrollbars during setscale
var tx = this.view.translate.x;
var ty = this.view.translate.y;
var sl = this.container.scrollLeft;
var st = this.container.scrollTop;
this.view.setScale(scale);
if (hasScrollbars)
{
var dx = 0;
var dy = 0;
if (center)
{
dx = this.container.offsetWidth * (factor - 1) / 2;
dy = this.container.offsetHeight * (factor - 1) / 2;
}
this.container.scrollLeft = (this.view.translate.x - tx) * this.view.scale + Math.round(sl * factor + dx);
this.container.scrollTop = (this.view.translate.y - ty) * this.view.scale + Math.round(st * factor + dy);
}
}
}
};
/**
* Function: zoomToRect
*
* Zooms the graph to the specified rectangle. If the rectangle does not have same aspect
* ratio as the display container, it is increased in the smaller relative dimension only
* until the aspect match. The original rectangle is centralised within this expanded one.
*
* Note that the input rectangular must be un-scaled and un-translated.
*
* Parameters:
*
* rect - The un-scaled and un-translated rectangluar region that should be just visible
* after the operation
*/
mxGraph.prototype.zoomToRect = function(rect)
{
var scaleX = this.container.clientWidth / rect.width;
var scaleY = this.container.clientHeight / rect.height;
var aspectFactor = scaleX / scaleY;
// Remove any overlap of the rect outside the client area
rect.x = Math.max(0, rect.x);
rect.y = Math.max(0, rect.y);
var rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width);
var rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height);
rect.width = rectRight - rect.x;
rect.height = rectBottom - rect.y;
// The selection area has to be increased to the same aspect
// ratio as the container, centred around the centre point of the
// original rect passed in.
if (aspectFactor < 1.0)
{
// Height needs increasing
var newHeight = rect.height / aspectFactor;
var deltaHeightBuffer = (newHeight - rect.height) / 2.0;
rect.height = newHeight;
// Assign up to half the buffer to the upper part of the rect, not crossing 0
// put the rest on the bottom
var upperBuffer = Math.min(rect.y , deltaHeightBuffer);
rect.y = rect.y - upperBuffer;
// Check if the bottom has extended too far
rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height);
rect.height = rectBottom - rect.y;
}
else
{
// Width needs increasing
var newWidth = rect.width * aspectFactor;
var deltaWidthBuffer = (newWidth - rect.width) / 2.0;
rect.width = newWidth;
// Assign up to half the buffer to the upper part of the rect, not crossing 0
// put the rest on the bottom
var leftBuffer = Math.min(rect.x , deltaWidthBuffer);
rect.x = rect.x - leftBuffer;
// Check if the right hand side has extended too far
rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width);
rect.width = rectRight - rect.x;
}
var scale = this.container.clientWidth / rect.width;
var newScale = this.view.scale * scale;
if (!mxUtils.hasScrollbars(this.container))
{
this.view.scaleAndTranslate(newScale, (this.view.translate.x - rect.x / this.view.scale), (this.view.translate.y - rect.y / this.view.scale));
}
else
{
this.view.setScale(newScale);
this.container.scrollLeft = Math.round(rect.x * scale);
this.container.scrollTop = Math.round(rect.y * scale);
}
};
/**
* Function: scrollCellToVisible
*
* Pans the graph so that it shows the given cell. Optionally the cell may
* be centered in the container.
*
* To center a given graph if the <container> has no scrollbars, use the following code.
*
* [code]
* var bounds = graph.getGraphBounds();
* graph.view.setTranslate(-bounds.x - (bounds.width - container.clientWidth) / 2,
* -bounds.y - (bounds.height - container.clientHeight) / 2);
* [/code]
*
* Parameters:
*
* cell - <mxCell> to be made visible.
* center - Optional boolean flag. Default is false.
*/
mxGraph.prototype.scrollCellToVisible = function(cell, center)
{
var x = -this.view.translate.x;
var y = -this.view.translate.y;
var state = this.view.getState(cell);
if (state != null)
{
var bounds = new mxRectangle(x + state.x, y + state.y, state.width,
state.height);
if (center && this.container != null)
{
var w = this.container.clientWidth;
var h = this.container.clientHeight;
bounds.x = bounds.getCenterX() - w / 2;
bounds.width = w;
bounds.y = bounds.getCenterY() - h / 2;
bounds.height = h;
}
var tr = new mxPoint(this.view.translate.x, this.view.translate.y);
if (this.scrollRectToVisible(bounds))
{
// Triggers an update via the view's event source
var tr2 = new mxPoint(this.view.translate.x, this.view.translate.y);
this.view.translate.x = tr.x;
this.view.translate.y = tr.y;
this.view.setTranslate(tr2.x, tr2.y);
}
}
};
/**
* Function: scrollRectToVisible
*
* Pans the graph so that it shows the given rectangle.
*
* Parameters:
*
* rect - <mxRectangle> to be made visible.
*/
mxGraph.prototype.scrollRectToVisible = function(rect)
{
var isChanged = false;
if (rect != null)
{
var w = this.container.offsetWidth;
var h = this.container.offsetHeight;
var widthLimit = Math.min(w, rect.width);
var heightLimit = Math.min(h, rect.height);
if (mxUtils.hasScrollbars(this.container))
{
var c = this.container;
rect.x += this.view.translate.x;
rect.y += this.view.translate.y;
var dx = c.scrollLeft - rect.x;
var ddx = Math.max(dx - c.scrollLeft, 0);
if (dx > 0)
{
c.scrollLeft -= dx + 2;
}
else
{
dx = rect.x + widthLimit - c.scrollLeft - c.clientWidth;
if (dx > 0)
{
c.scrollLeft += dx + 2;
}
}
var dy = c.scrollTop - rect.y;
var ddy = Math.max(0, dy - c.scrollTop);
if (dy > 0)
{
c.scrollTop -= dy + 2;
}
else
{
dy = rect.y + heightLimit - c.scrollTop - c.clientHeight;
if (dy > 0)
{
c.scrollTop += dy + 2;
}
}
if (!this.useScrollbarsForPanning && (ddx != 0 || ddy != 0))
{
this.view.setTranslate(ddx, ddy);
}
}
else
{
var x = -this.view.translate.x;
var y = -this.view.translate.y;
var s = this.view.scale;
if (rect.x + widthLimit > x + w)
{
this.view.translate.x -= (rect.x + widthLimit - w - x) / s;
isChanged = true;
}
if (rect.y + heightLimit > y + h)
{
this.view.translate.y -= (rect.y + heightLimit - h - y) / s;
isChanged = true;
}
if (rect.x < x)
{
this.view.translate.x += (x - rect.x) / s;
isChanged = true;
}
if (rect.y < y)
{
this.view.translate.y += (y - rect.y) / s;
isChanged = true;
}
if (isChanged)
{
this.view.refresh();
// Repaints selection marker (ticket 18)
if (this.selectionCellsHandler != null)
{
this.selectionCellsHandler.refresh();
}
}
}
}
return isChanged;
};
/**
* Function: getCellGeometry
*
* Returns the <mxGeometry> for the given cell. This implementation uses
* <mxGraphModel.getGeometry>. Subclasses can override this to implement
* specific geometries for cells in only one graph, that is, it can return
* geometries that depend on the current state of the view.
*
* Parameters:
*
* cell - <mxCell> whose geometry should be returned.
*/
mxGraph.prototype.getCellGeometry = function(cell)
{
return this.model.getGeometry(cell);
};
/**
* Function: isCellVisible
*
* Returns true if the given cell is visible in this graph. This
* implementation uses <mxGraphModel.isVisible>. Subclassers can override
* this to implement specific visibility for cells in only one graph, that
* is, without affecting the visible state of the cell.
*
* When using dynamic filter expressions for cell visibility, then the
* graph should be revalidated after the filter expression has changed.
*
* Parameters:
*
* cell - <mxCell> whose visible state should be returned.
*/
mxGraph.prototype.isCellVisible = function(cell)
{
return this.model.isVisible(cell);
};
/**
* Function: isCellCollapsed
*
* Returns true if the given cell is collapsed in this graph. This
* implementation uses <mxGraphModel.isCollapsed>. Subclassers can override
* this to implement specific collapsed states for cells in only one graph,
* that is, without affecting the collapsed state of the cell.
*
* When using dynamic filter expressions for the collapsed state, then the
* graph should be revalidated after the filter expression has changed.
*
* Parameters:
*
* cell - <mxCell> whose collapsed state should be returned.
*/
mxGraph.prototype.isCellCollapsed = function(cell)
{
return this.model.isCollapsed(cell);
};
/**
* Function: isCellConnectable
*
* Returns true if the given cell is connectable in this graph. This
* implementation uses <mxGraphModel.isConnectable>. Subclassers can override
* this to implement specific connectable states for cells in only one graph,
* that is, without affecting the connectable state of the cell in the model.
*
* Parameters:
*
* cell - <mxCell> whose connectable state should be returned.
*/
mxGraph.prototype.isCellConnectable = function(cell)
{
return this.model.isConnectable(cell);
};
/**
* Function: isOrthogonal
*
* Returns true if perimeter points should be computed such that the
* resulting edge has only horizontal or vertical segments.
*
* Parameters:
*
* edge - <mxCellState> that represents the edge.
*/
mxGraph.prototype.isOrthogonal = function(edge)
{
var orthogonal = edge.style[mxConstants.STYLE_ORTHOGONAL];
if (orthogonal != null)
{
return orthogonal;
}
var tmp = this.view.getEdgeStyle(edge);
return tmp == mxEdgeStyle.SegmentConnector ||
tmp == mxEdgeStyle.ElbowConnector ||
tmp == mxEdgeStyle.SideToSide ||
tmp == mxEdgeStyle.TopToBottom ||
tmp == mxEdgeStyle.EntityRelation ||
tmp == mxEdgeStyle.OrthConnector;
};
/**
* Function: isLoop
*
* Returns true if the given cell state is a loop.
*
* Parameters:
*
* state - <mxCellState> that represents a potential loop.
*/
mxGraph.prototype.isLoop = function(state)
{
var src = state.getVisibleTerminalState(true);
var trg = state.getVisibleTerminalState(false);
return (src != null && src == trg);
};
/**
* Function: isCloneEvent
*
* Returns true if the given event is a clone event. This implementation
* returns true if control is pressed.
*/
mxGraph.prototype.isCloneEvent = function(evt)
{
return mxEvent.isControlDown(evt);
};
/**
* Function: isTransparentClickEvent
*
* Hook for implementing click-through behaviour on selected cells. If this
* returns true the cell behind the selected cell will be selected. This
* implementation returns false;
*/
mxGraph.prototype.isTransparentClickEvent = function(evt)
{
return false;
};
/**
* Function: isToggleEvent
*
* Returns true if the given event is a toggle event. This implementation
* returns true if the meta key (Cmd) is pressed on Macs or if control is
* pressed on any other platform.
*/
mxGraph.prototype.isToggleEvent = function(evt)
{
return (mxClient.IS_MAC) ? mxEvent.isMetaDown(evt) : mxEvent.isControlDown(evt);
};
/**
* Function: isGridEnabledEvent
*
* Returns true if the given mouse event should be aligned to the grid.
*/
mxGraph.prototype.isGridEnabledEvent = function(evt)
{
return evt != null && !mxEvent.isAltDown(evt);
};
/**
* Function: isConstrainedEvent
*
* Returns true if the given mouse event should be aligned to the grid.
*/
mxGraph.prototype.isConstrainedEvent = function(evt)
{
return mxEvent.isShiftDown(evt);
};
/**
* Function: isIgnoreTerminalEvent
*
* Returns true if the given mouse event should not allow any connections to be
* made. This implementation returns false.
*/
mxGraph.prototype.isIgnoreTerminalEvent = function(evt)
{
return false;
};
/**
* Group: Validation
*/
/**
* Function: validationAlert
*
* Displays the given validation error in a dialog. This implementation uses
* mxUtils.alert.
*/
mxGraph.prototype.validationAlert = function(message)
{
mxUtils.alert(message);
};
/**
* Function: isEdgeValid
*
* Checks if the return value of <getEdgeValidationError> for the given
* arguments is null.
*
* Parameters:
*
* edge - <mxCell> that represents the edge to validate.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
*/
mxGraph.prototype.isEdgeValid = function(edge, source, target)
{
return this.getEdgeValidationError(edge, source, target) == null;
};
/**
* Function: getEdgeValidationError
*
* Returns the validation error message to be displayed when inserting or
* changing an edges' connectivity. A return value of null means the edge
* is valid, a return value of '' means it's not valid, but do not display
* an error message. Any other (non-empty) string returned from this method
* is displayed as an error message when trying to connect an edge to a
* source and target. This implementation uses the <multiplicities>, and
* checks <multigraph>, <allowDanglingEdges> and <allowLoops> to generate
* validation errors.
*
* For extending this method with specific checks for source/target cells,
* the method can be extended as follows. Returning an empty string means
* the edge is invalid with no error message, a non-null string specifies
* the error message, and null means the edge is valid.
*
* (code)
* graph.getEdgeValidationError = function(edge, source, target)
* {
* if (source != null && target != null &&
* this.model.getValue(source) != null &&
* this.model.getValue(target) != null)
* {
* if (target is not valid for source)
* {
* return 'Invalid Target';
* }
* }
*
* // "Supercall"
* return mxGraph.prototype.getEdgeValidationError.apply(this, arguments);
* }
* (end)
*
* Parameters:
*
* edge - <mxCell> that represents the edge to validate.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
*/
mxGraph.prototype.getEdgeValidationError = function(edge, source, target)
{
if (edge != null && !this.isAllowDanglingEdges() && (source == null || target == null))
{
return '';
}
if (edge != null && this.model.getTerminal(edge, true) == null &&
this.model.getTerminal(edge, false) == null)
{
return null;
}
// Checks if we're dealing with a loop
if (!this.allowLoops && source == target && source != null)
{
return '';
}
// Checks if the connection is generally allowed
if (!this.isValidConnection(source, target))
{
return '';
}
if (source != null && target != null)
{
var error = '';
// Checks if the cells are already connected
// and adds an error message if required
if (!this.multigraph)
{
var tmp = this.model.getEdgesBetween(source, target, true);
// Checks if the source and target are not connected by another edge
if (tmp.length > 1 || (tmp.length == 1 && tmp[0] != edge))
{
error += (mxResources.get(this.alreadyConnectedResource) ||
this.alreadyConnectedResource)+'\n';
}
}
// Gets the number of outgoing edges from the source
// and the number of incoming edges from the target
// without counting the edge being currently changed.
var sourceOut = this.model.getDirectedEdgeCount(source, true, edge);
var targetIn = this.model.getDirectedEdgeCount(target, false, edge);
// Checks the change against each multiplicity rule
if (this.multiplicities != null)
{
for (var i = 0; i < this.multiplicities.length; i++)
{
var err = this.multiplicities[i].check(this, edge, source,
target, sourceOut, targetIn);
if (err != null)
{
error += err;
}
}
}
// Validates the source and target terminals independently
var err = this.validateEdge(edge, source, target);
if (err != null)
{
error += err;
}
return (error.length > 0) ? error : null;
}
return (this.allowDanglingEdges) ? null : '';
};
/**
* Function: validateEdge
*
* Hook method for subclassers to return an error message for the given
* edge and terminals. This implementation returns null.
*
* Parameters:
*
* edge - <mxCell> that represents the edge to validate.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
*/
mxGraph.prototype.validateEdge = function(edge, source, target)
{
return null;
};
/**
* Function: validateGraph
*
* Validates the graph by validating each descendant of the given cell or
* the root of the model. Context is an object that contains the validation
* state for the complete validation run. The validation errors are
* attached to their cells using <setCellWarning>. Returns null in the case of
* successful validation or an array of strings (warnings) in the case of
* failed validations.
*
* Paramters:
*
* cell - Optional <mxCell> to start the validation recursion. Default is
* the graph root.
* context - Object that represents the global validation state.
*/
mxGraph.prototype.validateGraph = function(cell, context)
{
cell = (cell != null) ? cell : this.model.getRoot();
context = (context != null) ? context : new Object();
var isValid = true;
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var tmp = this.model.getChildAt(cell, i);
var ctx = context;
if (this.isValidRoot(tmp))
{
ctx = new Object();
}
var warn = this.validateGraph(tmp, ctx);
if (warn != null)
{
this.setCellWarning(tmp, warn.replace(/\n/g, '<br>'));
}
else
{
this.setCellWarning(tmp, null);
}
isValid = isValid && warn == null;
}
var warning = '';
// Adds error for invalid children if collapsed (children invisible)
if (this.isCellCollapsed(cell) && !isValid)
{
warning += (mxResources.get(this.containsValidationErrorsResource) ||
this.containsValidationErrorsResource) + '\n';
}
// Checks edges and cells using the defined multiplicities
if (this.model.isEdge(cell))
{
warning += this.getEdgeValidationError(cell,
this.model.getTerminal(cell, true),
this.model.getTerminal(cell, false)) || '';
}
else
{
warning += this.getCellValidationError(cell) || '';
}
// Checks custom validation rules
var err = this.validateCell(cell, context);
if (err != null)
{
warning += err;
}
// Updates the display with the warning icons
// before any potential alerts are displayed.
// LATER: Move this into addCellOverlay. Redraw
// should check if overlay was added or removed.
if (this.model.getParent(cell) == null)
{
this.view.validate();
}
return (warning.length > 0 || !isValid) ? warning : null;
};
/**
* Function: getCellValidationError
*
* Checks all <multiplicities> that cannot be enforced while the graph is
* being modified, namely, all multiplicities that require a minimum of
* 1 edge.
*
* Parameters:
*
* cell - <mxCell> for which the multiplicities should be checked.
*/
mxGraph.prototype.getCellValidationError = function(cell)
{
var outCount = this.model.getDirectedEdgeCount(cell, true);
var inCount = this.model.getDirectedEdgeCount(cell, false);
var value = this.model.getValue(cell);
var error = '';
if (this.multiplicities != null)
{
for (var i = 0; i < this.multiplicities.length; i++)
{
var rule = this.multiplicities[i];
if (rule.source && mxUtils.isNode(value, rule.type,
rule.attr, rule.value) && (outCount > rule.max ||
outCount < rule.min))
{
error += rule.countError + '\n';
}
else if (!rule.source && mxUtils.isNode(value, rule.type,
rule.attr, rule.value) && (inCount > rule.max ||
inCount < rule.min))
{
error += rule.countError + '\n';
}
}
}
return (error.length > 0) ? error : null;
};
/**
* Function: validateCell
*
* Hook method for subclassers to return an error message for the given
* cell and validation context. This implementation returns null. Any HTML
* breaks will be converted to linefeeds in the calling method.
*
* Parameters:
*
* cell - <mxCell> that represents the cell to validate.
* context - Object that represents the global validation state.
*/
mxGraph.prototype.validateCell = function(cell, context)
{
return null;
};
/**
* Group: Graph appearance
*/
/**
* Function: getBackgroundImage
*
* Returns the <backgroundImage> as an <mxImage>.
*/
mxGraph.prototype.getBackgroundImage = function()
{
return this.backgroundImage;
};
/**
* Function: setBackgroundImage
*
* Sets the new <backgroundImage>.
*
* Parameters:
*
* image - New <mxImage> to be used for the background.
*/
mxGraph.prototype.setBackgroundImage = function(image)
{
this.backgroundImage = image;
};
/**
* Function: getFoldingImage
*
* Returns the <mxImage> used to display the collapsed state of
* the specified cell state. This returns null for all edges.
*/
mxGraph.prototype.getFoldingImage = function(state)
{
if (state != null && this.foldingEnabled && !this.getModel().isEdge(state.cell))
{
var tmp = this.isCellCollapsed(state.cell);
if (this.isCellFoldable(state.cell, !tmp))
{
return (tmp) ? this.collapsedImage : this.expandedImage;
}
}
return null;
};
/**
* Function: convertValueToString
*
* Returns the textual representation for the given cell. This
* implementation returns the nodename or string-representation of the user
* object.
*
* Example:
*
* The following returns the label attribute from the cells user
* object if it is an XML node.
*
* (code)
* graph.convertValueToString = function(cell)
* {
* return cell.getAttribute('label');
* }
* (end)
*
* See also: <cellLabelChanged>.
*
* Parameters:
*
* cell - <mxCell> whose textual representation should be returned.
*/
mxGraph.prototype.convertValueToString = function(cell)
{
var value = this.model.getValue(cell);
if (value != null)
{
if (mxUtils.isNode(value))
{
return value.nodeName;
}
else if (typeof(value.toString) == 'function')
{
return value.toString();
}
}
return '';
};
/**
* Function: getLabel
*
* Returns a string or DOM node that represents the label for the given
* cell. This implementation uses <convertValueToString> if <labelsVisible>
* is true. Otherwise it returns an empty string.
*
* To truncate a label to match the size of the cell, the following code
* can be used.
*
* (code)
* graph.getLabel = function(cell)
* {
* var label = mxGraph.prototype.getLabel.apply(this, arguments);
*
* if (label != null && this.model.isVertex(cell))
* {
* var geo = this.getCellGeometry(cell);
*
* if (geo != null)
* {
* var max = parseInt(geo.width / 8);
*
* if (label.length > max)
* {
* label = label.substring(0, max)+'...';
* }
* }
* }
* return mxUtils.htmlEntities(label);
* }
* (end)
*
* A resize listener is needed in the graph to force a repaint of the label
* after a resize.
*
* (code)
* graph.addListener(mxEvent.RESIZE_CELLS, function(sender, evt)
* {
* var cells = evt.getProperty('cells');
*
* for (var i = 0; i < cells.length; i++)
* {
* this.view.removeState(cells[i]);
* }
* });
* (end)
*
* Parameters:
*
* cell - <mxCell> whose label should be returned.
*/
mxGraph.prototype.getLabel = function(cell)
{
var result = '';
if (this.labelsVisible && cell != null)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
if (!mxUtils.getValue(style, mxConstants.STYLE_NOLABEL, false))
{
result = this.convertValueToString(cell);
}
}
return result;
};
/**
* Function: isHtmlLabel
*
* Returns true if the label must be rendered as HTML markup. The default
* implementation returns <htmlLabels>.
*
* Parameters:
*
* cell - <mxCell> whose label should be displayed as HTML markup.
*/
mxGraph.prototype.isHtmlLabel = function(cell)
{
return this.isHtmlLabels();
};
/**
* Function: isHtmlLabels
*
* Returns <htmlLabels>.
*/
mxGraph.prototype.isHtmlLabels = function()
{
return this.htmlLabels;
};
/**
* Function: setHtmlLabels
*
* Sets <htmlLabels>.
*/
mxGraph.prototype.setHtmlLabels = function(value)
{
this.htmlLabels = value;
};
/**
* Function: isWrapping
*
* This enables wrapping for HTML labels.
*
* Returns true if no white-space CSS style directive should be used for
* displaying the given cells label. This implementation returns true if
* <mxConstants.STYLE_WHITE_SPACE> in the style of the given cell is 'wrap'.
*
* This is used as a workaround for IE ignoring the white-space directive
* of child elements if the directive appears in a parent element. It
* should be overridden to return true if a white-space directive is used
* in the HTML markup that represents the given cells label. In order for
* HTML markup to work in labels, <isHtmlLabel> must also return true
* for the given cell.
*
* Example:
*
* (code)
* graph.getLabel = function(cell)
* {
* var tmp = mxGraph.prototype.getLabel.apply(this, arguments); // "supercall"
*
* if (this.model.isEdge(cell))
* {
* tmp = '<div style="width: 150px; white-space:normal;">'+tmp+'</div>';
* }
*
* return tmp;
* }
*
* graph.isWrapping = function(state)
* {
* return this.model.isEdge(state.cell);
* }
* (end)
*
* Makes sure no edge label is wider than 150 pixels, otherwise the content
* is wrapped. Note: No width must be specified for wrapped vertex labels as
* the vertex defines the width in its geometry.
*
* Parameters:
*
* state - <mxCell> whose label should be wrapped.
*/
mxGraph.prototype.isWrapping = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return (style != null) ? style[mxConstants.STYLE_WHITE_SPACE] == 'wrap' : false;
};
/**
* Function: isLabelClipped
*
* Returns true if the overflow portion of labels should be hidden. If this
* returns true then vertex labels will be clipped to the size of the vertices.
* This implementation returns true if <mxConstants.STYLE_OVERFLOW> in the
* style of the given cell is 'hidden'.
*
* Parameters:
*
* state - <mxCell> whose label should be clipped.
*/
mxGraph.prototype.isLabelClipped = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return (style != null) ? style[mxConstants.STYLE_OVERFLOW] == 'hidden' : false;
};
/**
* Function: getTooltip
*
* Returns the string or DOM node that represents the tooltip for the given
* state, node and coordinate pair. This implementation checks if the given
* node is a folding icon or overlay and returns the respective tooltip. If
* this does not result in a tooltip, the handler for the cell is retrieved
* from <selectionCellsHandler> and the optional getTooltipForNode method is
* called. If no special tooltip exists here then <getTooltipForCell> is used
* with the cell in the given state as the argument to return a tooltip for the
* given state.
*
* Parameters:
*
* state - <mxCellState> whose tooltip should be returned.
* node - DOM node that is currently under the mouse.
* x - X-coordinate of the mouse.
* y - Y-coordinate of the mouse.
*/
mxGraph.prototype.getTooltip = function(state, node, x, y)
{
var tip = null;
if (state != null)
{
// Checks if the mouse is over the folding icon
if (state.control != null && (node == state.control.node ||
node.parentNode == state.control.node))
{
tip = this.collapseExpandResource;
tip = mxUtils.htmlEntities(mxResources.get(tip) || tip).replace(/\\n/g, '<br>');
}
if (tip == null && state.overlays != null)
{
state.overlays.visit(function(id, shape)
{
// LATER: Exit loop if tip is not null
if (tip == null && (node == shape.node || node.parentNode == shape.node))
{
tip = shape.overlay.toString();
}
});
}
if (tip == null)
{
var handler = this.selectionCellsHandler.getHandler(state.cell);
if (handler != null && typeof(handler.getTooltipForNode) == 'function')
{
tip = handler.getTooltipForNode(node);
}
}
if (tip == null)
{
tip = this.getTooltipForCell(state.cell);
}
}
return tip;
};
/**
* Function: getTooltipForCell
*
* Returns the string or DOM node to be used as the tooltip for the given
* cell. This implementation uses the cells getTooltip function if it
* exists, or else it returns <convertValueToString> for the cell.
*
* Example:
*
* (code)
* graph.getTooltipForCell = function(cell)
* {
* return 'Hello, World!';
* }
* (end)
*
* Replaces all tooltips with the string Hello, World!
*
* Parameters:
*
* cell - <mxCell> whose tooltip should be returned.
*/
mxGraph.prototype.getTooltipForCell = function(cell)
{
var tip = null;
if (cell != null && cell.getTooltip != null)
{
tip = cell.getTooltip();
}
else
{
tip = this.convertValueToString(cell);
}
return tip;
};
/**
* Function: getLinkForCell
*
* Returns the string to be used as the link for the given cell. This
* implementation returns null.
*
* Parameters:
*
* cell - <mxCell> whose tooltip should be returned.
*/
mxGraph.prototype.getLinkForCell = function(cell)
{
return null;
};
/**
* Function: getCursorForMouseEvent
*
* Returns the cursor value to be used for the CSS of the shape for the
* given event. This implementation calls <getCursorForCell>.
*
* Parameters:
*
* me - <mxMouseEvent> whose cursor should be returned.
*/
mxGraph.prototype.getCursorForMouseEvent = function(me)
{
return this.getCursorForCell(me.getCell());
};
/**
* Function: getCursorForCell
*
* Returns the cursor value to be used for the CSS of the shape for the
* given cell. This implementation returns null.
*
* Parameters:
*
* cell - <mxCell> whose cursor should be returned.
*/
mxGraph.prototype.getCursorForCell = function(cell)
{
return null;
};
/**
* Function: getStartSize
*
* Returns the start size of the given swimlane, that is, the width or
* height of the part that contains the title, depending on the
* horizontal style. The return value is an <mxRectangle> with either
* width or height set as appropriate.
*
* Parameters:
*
* swimlane - <mxCell> whose start size should be returned.
*/
mxGraph.prototype.getStartSize = function(swimlane)
{
var result = new mxRectangle();
var state = this.view.getState(swimlane);
var style = (state != null) ? state.style : this.getCellStyle(swimlane);
if (style != null)
{
var size = parseInt(mxUtils.getValue(style,
mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE));
if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true))
{
result.height = size;
}
else
{
result.width = size;
}
}
return result;
};
/**
* Function: getImage
*
* Returns the image URL for the given cell state. This implementation
* returns the value stored under <mxConstants.STYLE_IMAGE> in the cell
* style.
*
* Parameters:
*
* state - <mxCellState> whose image URL should be returned.
*/
mxGraph.prototype.getImage = function(state)
{
return (state != null && state.style != null) ? state.style[mxConstants.STYLE_IMAGE] : null;
};
/**
* Function: getVerticalAlign
*
* Returns the vertical alignment for the given cell state. This
* implementation returns the value stored under
* <mxConstants.STYLE_VERTICAL_ALIGN> in the cell style.
*
* Parameters:
*
* state - <mxCellState> whose vertical alignment should be
* returned.
*/
mxGraph.prototype.getVerticalAlign = function(state)
{
return (state != null && state.style != null) ?
(state.style[mxConstants.STYLE_VERTICAL_ALIGN] ||
mxConstants.ALIGN_MIDDLE) : null;
};
/**
* Function: getIndicatorColor
*
* Returns the indicator color for the given cell state. This
* implementation returns the value stored under
* <mxConstants.STYLE_INDICATOR_COLOR> in the cell style.
*
* Parameters:
*
* state - <mxCellState> whose indicator color should be
* returned.
*/
mxGraph.prototype.getIndicatorColor = function(state)
{
return (state != null && state.style != null) ? state.style[mxConstants.STYLE_INDICATOR_COLOR] : null;
};
/**
* Function: getIndicatorGradientColor
*
* Returns the indicator gradient color for the given cell state. This
* implementation returns the value stored under
* <mxConstants.STYLE_INDICATOR_GRADIENTCOLOR> in the cell style.
*
* Parameters:
*
* state - <mxCellState> whose indicator gradient color should be
* returned.
*/
mxGraph.prototype.getIndicatorGradientColor = function(state)
{
return (state != null && state.style != null) ? state.style[mxConstants.STYLE_INDICATOR_GRADIENTCOLOR] : null;
};
/**
* Function: getIndicatorShape
*
* Returns the indicator shape for the given cell state. This
* implementation returns the value stored under
* <mxConstants.STYLE_INDICATOR_SHAPE> in the cell style.
*
* Parameters:
*
* state - <mxCellState> whose indicator shape should be returned.
*/
mxGraph.prototype.getIndicatorShape = function(state)
{
return (state != null && state.style != null) ? state.style[mxConstants.STYLE_INDICATOR_SHAPE] : null;
};
/**
* Function: getIndicatorImage
*
* Returns the indicator image for the given cell state. This
* implementation returns the value stored under
* <mxConstants.STYLE_INDICATOR_IMAGE> in the cell style.
*
* Parameters:
*
* state - <mxCellState> whose indicator image should be returned.
*/
mxGraph.prototype.getIndicatorImage = function(state)
{
return (state != null && state.style != null) ? state.style[mxConstants.STYLE_INDICATOR_IMAGE] : null;
};
/**
* Function: getBorder
*
* Returns the value of <border>.
*/
mxGraph.prototype.getBorder = function()
{
return this.border;
};
/**
* Function: setBorder
*
* Sets the value of <border>.
*
* Parameters:
*
* value - Positive integer that represents the border to be used.
*/
mxGraph.prototype.setBorder = function(value)
{
this.border = value;
};
/**
* Function: isSwimlane
*
* Returns true if the given cell is a swimlane in the graph. A swimlane is
* a container cell with some specific behaviour. This implementation
* checks if the shape associated with the given cell is a <mxSwimlane>.
*
* Parameters:
*
* cell - <mxCell> to be checked.
*/
mxGraph.prototype.isSwimlane = function (cell)
{
if (cell != null)
{
if (this.model.getParent(cell) != this.model.getRoot())
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
if (style != null && !this.model.isEdge(cell))
{
return style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_SWIMLANE;
}
}
}
return false;
};
/**
* Group: Graph behaviour
*/
/**
* Function: isResizeContainer
*
* Returns <resizeContainer>.
*/
mxGraph.prototype.isResizeContainer = function()
{
return this.resizeContainer;
};
/**
* Function: setResizeContainer
*
* Sets <resizeContainer>.
*
* Parameters:
*
* value - Boolean indicating if the container should be resized.
*/
mxGraph.prototype.setResizeContainer = function(value)
{
this.resizeContainer = value;
};
/**
* Function: isEnabled
*
* Returns true if the graph is <enabled>.
*/
mxGraph.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Specifies if the graph should allow any interactions. This
* implementation updates <enabled>.
*
* Parameters:
*
* value - Boolean indicating if the graph should be enabled.
*/
mxGraph.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: isEscapeEnabled
*
* Returns <escapeEnabled>.
*/
mxGraph.prototype.isEscapeEnabled = function()
{
return this.escapeEnabled;
};
/**
* Function: setEscapeEnabled
*
* Sets <escapeEnabled>.
*
* Parameters:
*
* enabled - Boolean indicating if escape should be enabled.
*/
mxGraph.prototype.setEscapeEnabled = function(value)
{
this.escapeEnabled = value;
};
/**
* Function: isInvokesStopCellEditing
*
* Returns <invokesStopCellEditing>.
*/
mxGraph.prototype.isInvokesStopCellEditing = function()
{
return this.invokesStopCellEditing;
};
/**
* Function: setInvokesStopCellEditing
*
* Sets <invokesStopCellEditing>.
*/
mxGraph.prototype.setInvokesStopCellEditing = function(value)
{
this.invokesStopCellEditing = value;
};
/**
* Function: isEnterStopsCellEditing
*
* Returns <enterStopsCellEditing>.
*/
mxGraph.prototype.isEnterStopsCellEditing = function()
{
return this.enterStopsCellEditing;
};
/**
* Function: setEnterStopsCellEditing
*
* Sets <enterStopsCellEditing>.
*/
mxGraph.prototype.setEnterStopsCellEditing = function(value)
{
this.enterStopsCellEditing = value;
};
/**
* Function: isCellLocked
*
* Returns true if the given cell may not be moved, sized, bended,
* disconnected, edited or selected. This implementation returns true for
* all vertices with a relative geometry if <locked> is false.
*
* Parameters:
*
* cell - <mxCell> whose locked state should be returned.
*/
mxGraph.prototype.isCellLocked = function(cell)
{
var geometry = this.model.getGeometry(cell);
return this.isCellsLocked() || (geometry != null && this.model.isVertex(cell) && geometry.relative);
};
/**
* Function: isCellsLocked
*
* Returns true if the given cell may not be moved, sized, bended,
* disconnected, edited or selected. This implementation returns true for
* all vertices with a relative geometry if <locked> is false.
*
* Parameters:
*
* cell - <mxCell> whose locked state should be returned.
*/
mxGraph.prototype.isCellsLocked = function()
{
return this.cellsLocked;
};
/**
* Function: setCellsLocked
*
* Sets if any cell may be moved, sized, bended, disconnected, edited or
* selected.
*
* Parameters:
*
* value - Boolean that defines the new value for <cellsLocked>.
*/
mxGraph.prototype.setCellsLocked = function(value)
{
this.cellsLocked = value;
};
/**
* Function: getCloneableCells
*
* Returns the cells which may be exported in the given array of cells.
*/
mxGraph.prototype.getCloneableCells = function(cells)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.isCellCloneable(cell);
}));
};
/**
* Function: isCellCloneable
*
* Returns true if the given cell is cloneable. This implementation returns
* <isCellsCloneable> for all cells unless a cell style specifies
* <mxConstants.STYLE_CLONEABLE> to be 0.
*
* Parameters:
*
* cell - Optional <mxCell> whose cloneable state should be returned.
*/
mxGraph.prototype.isCellCloneable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsCloneable() && style[mxConstants.STYLE_CLONEABLE] != 0;
};
/**
* Function: isCellsCloneable
*
* Returns <cellsCloneable>, that is, if the graph allows cloning of cells
* by using control-drag.
*/
mxGraph.prototype.isCellsCloneable = function()
{
return this.cellsCloneable;
};
/**
* Function: setCellsCloneable
*
* Specifies if the graph should allow cloning of cells by holding down the
* control key while cells are being moved. This implementation updates
* <cellsCloneable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should be cloneable.
*/
mxGraph.prototype.setCellsCloneable = function(value)
{
this.cellsCloneable = value;
};
/**
* Function: getExportableCells
*
* Returns the cells which may be exported in the given array of cells.
*/
mxGraph.prototype.getExportableCells = function(cells)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.canExportCell(cell);
}));
};
/**
* Function: canExportCell
*
* Returns true if the given cell may be exported to the clipboard. This
* implementation returns <exportEnabled> for all cells.
*
* Parameters:
*
* cell - <mxCell> that represents the cell to be exported.
*/
mxGraph.prototype.canExportCell = function(cell)
{
return this.exportEnabled;
};
/**
* Function: getImportableCells
*
* Returns the cells which may be imported in the given array of cells.
*/
mxGraph.prototype.getImportableCells = function(cells)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.canImportCell(cell);
}));
};
/**
* Function: canImportCell
*
* Returns true if the given cell may be imported from the clipboard.
* This implementation returns <importEnabled> for all cells.
*
* Parameters:
*
* cell - <mxCell> that represents the cell to be imported.
*/
mxGraph.prototype.canImportCell = function(cell)
{
return this.importEnabled;
};
/**
* Function: isCellSelectable
*
* Returns true if the given cell is selectable. This implementation
* returns <cellsSelectable>.
*
* To add a new style for making cells (un)selectable, use the following code.
*
* (code)
* mxGraph.prototype.isCellSelectable = function(cell)
* {
* var state = this.view.getState(cell);
* var style = (state != null) ? state.style : this.getCellStyle(cell);
*
* return this.isCellsSelectable() && !this.isCellLocked(cell) && style['selectable'] != 0;
* };
* (end)
*
* You can then use the new style as shown in this example.
*
* (code)
* graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'selectable=0');
* (end)
*
* Parameters:
*
* cell - <mxCell> whose selectable state should be returned.
*/
mxGraph.prototype.isCellSelectable = function(cell)
{
return this.isCellsSelectable();
};
/**
* Function: isCellsSelectable
*
* Returns <cellsSelectable>.
*/
mxGraph.prototype.isCellsSelectable = function()
{
return this.cellsSelectable;
};
/**
* Function: setCellsSelectable
*
* Sets <cellsSelectable>.
*/
mxGraph.prototype.setCellsSelectable = function(value)
{
this.cellsSelectable = value;
};
/**
* Function: getDeletableCells
*
* Returns the cells which may be exported in the given array of cells.
*/
mxGraph.prototype.getDeletableCells = function(cells)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.isCellDeletable(cell);
}));
};
/**
* Function: isCellDeletable
*
* Returns true if the given cell is moveable. This returns
* <cellsDeletable> for all given cells if a cells style does not specify
* <mxConstants.STYLE_DELETABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose deletable state should be returned.
*/
mxGraph.prototype.isCellDeletable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsDeletable() && style[mxConstants.STYLE_DELETABLE] != 0;
};
/**
* Function: isCellsDeletable
*
* Returns <cellsDeletable>.
*/
mxGraph.prototype.isCellsDeletable = function()
{
return this.cellsDeletable;
};
/**
* Function: setCellsDeletable
*
* Sets <cellsDeletable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should allow deletion of cells.
*/
mxGraph.prototype.setCellsDeletable = function(value)
{
this.cellsDeletable = value;
};
/**
* Function: isLabelMovable
*
* Returns true if the given edges's label is moveable. This returns
* <movable> for all given cells if <isLocked> does not return true
* for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose label should be moved.
*/
mxGraph.prototype.isLabelMovable = function(cell)
{
return !this.isCellLocked(cell) &&
((this.model.isEdge(cell) && this.edgeLabelsMovable) ||
(this.model.isVertex(cell) && this.vertexLabelsMovable));
};
/**
* Function: isCellRotatable
*
* Returns true if the given cell is rotatable. This returns true for the given
* cell if its style does not specify <mxConstants.STYLE_ROTATABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose rotatable state should be returned.
*/
mxGraph.prototype.isCellRotatable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return style[mxConstants.STYLE_ROTATABLE] != 0;
};
/**
* Function: getMovableCells
*
* Returns the cells which are movable in the given array of cells.
*/
mxGraph.prototype.getMovableCells = function(cells)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.isCellMovable(cell);
}));
};
/**
* Function: isCellMovable
*
* Returns true if the given cell is moveable. This returns <cellsMovable>
* for all given cells if <isCellLocked> does not return true for the given
* cell and its style does not specify <mxConstants.STYLE_MOVABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose movable state should be returned.
*/
mxGraph.prototype.isCellMovable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsMovable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_MOVABLE] != 0;
};
/**
* Function: isCellsMovable
*
* Returns <cellsMovable>.
*/
mxGraph.prototype.isCellsMovable = function()
{
return this.cellsMovable;
};
/**
* Function: setCellsMovable
*
* Specifies if the graph should allow moving of cells. This implementation
* updates <cellsMsovable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should allow moving of cells.
*/
mxGraph.prototype.setCellsMovable = function(value)
{
this.cellsMovable = value;
};
/**
* Function: isGridEnabled
*
* Returns <gridEnabled> as a boolean.
*/
mxGraph.prototype.isGridEnabled = function()
{
return this.gridEnabled;
};
/**
* Function: setGridEnabled
*
* Specifies if the grid should be enabled.
*
* Parameters:
*
* value - Boolean indicating if the grid should be enabled.
*/
mxGraph.prototype.setGridEnabled = function(value)
{
this.gridEnabled = value;
};
/**
* Function: isPortsEnabled
*
* Returns <portsEnabled> as a boolean.
*/
mxGraph.prototype.isPortsEnabled = function()
{
return this.portsEnabled;
};
/**
* Function: setPortsEnabled
*
* Specifies if the ports should be enabled.
*
* Parameters:
*
* value - Boolean indicating if the ports should be enabled.
*/
mxGraph.prototype.setPortsEnabled = function(value)
{
this.portsEnabled = value;
};
/**
* Function: getGridSize
*
* Returns <gridSize>.
*/
mxGraph.prototype.getGridSize = function()
{
return this.gridSize;
};
/**
* Function: setGridSize
*
* Sets <gridSize>.
*/
mxGraph.prototype.setGridSize = function(value)
{
this.gridSize = value;
};
/**
* Function: getTolerance
*
* Returns <tolerance>.
*/
mxGraph.prototype.getTolerance = function()
{
return this.tolerance;
};
/**
* Function: setTolerance
*
* Sets <tolerance>.
*/
mxGraph.prototype.setTolerance = function(value)
{
this.tolerance = value;
};
/**
* Function: isVertexLabelsMovable
*
* Returns <vertexLabelsMovable>.
*/
mxGraph.prototype.isVertexLabelsMovable = function()
{
return this.vertexLabelsMovable;
};
/**
* Function: setVertexLabelsMovable
*
* Sets <vertexLabelsMovable>.
*/
mxGraph.prototype.setVertexLabelsMovable = function(value)
{
this.vertexLabelsMovable = value;
};
/**
* Function: isEdgeLabelsMovable
*
* Returns <edgeLabelsMovable>.
*/
mxGraph.prototype.isEdgeLabelsMovable = function()
{
return this.edgeLabelsMovable;
};
/**
* Function: isEdgeLabelsMovable
*
* Sets <edgeLabelsMovable>.
*/
mxGraph.prototype.setEdgeLabelsMovable = function(value)
{
this.edgeLabelsMovable = value;
};
/**
* Function: isSwimlaneNesting
*
* Returns <swimlaneNesting> as a boolean.
*/
mxGraph.prototype.isSwimlaneNesting = function()
{
return this.swimlaneNesting;
};
/**
* Function: setSwimlaneNesting
*
* Specifies if swimlanes can be nested by drag and drop. This is only
* taken into account if dropEnabled is true.
*
* Parameters:
*
* value - Boolean indicating if swimlanes can be nested.
*/
mxGraph.prototype.setSwimlaneNesting = function(value)
{
this.swimlaneNesting = value;
};
/**
* Function: isSwimlaneSelectionEnabled
*
* Returns <swimlaneSelectionEnabled> as a boolean.
*/
mxGraph.prototype.isSwimlaneSelectionEnabled = function()
{
return this.swimlaneSelectionEnabled;
};
/**
* Function: setSwimlaneSelectionEnabled
*
* Specifies if swimlanes should be selected if the mouse is released
* over their content area.
*
* Parameters:
*
* value - Boolean indicating if swimlanes content areas
* should be selected when the mouse is released over them.
*/
mxGraph.prototype.setSwimlaneSelectionEnabled = function(value)
{
this.swimlaneSelectionEnabled = value;
};
/**
* Function: isMultigraph
*
* Returns <multigraph> as a boolean.
*/
mxGraph.prototype.isMultigraph = function()
{
return this.multigraph;
};
/**
* Function: setMultigraph
*
* Specifies if the graph should allow multiple connections between the
* same pair of vertices.
*
* Parameters:
*
* value - Boolean indicating if the graph allows multiple connections
* between the same pair of vertices.
*/
mxGraph.prototype.setMultigraph = function(value)
{
this.multigraph = value;
};
/**
* Function: isAllowLoops
*
* Returns <allowLoops> as a boolean.
*/
mxGraph.prototype.isAllowLoops = function()
{
return this.allowLoops;
};
/**
* Function: setAllowDanglingEdges
*
* Specifies if dangling edges are allowed, that is, if edges are allowed
* that do not have a source and/or target terminal defined.
*
* Parameters:
*
* value - Boolean indicating if dangling edges are allowed.
*/
mxGraph.prototype.setAllowDanglingEdges = function(value)
{
this.allowDanglingEdges = value;
};
/**
* Function: isAllowDanglingEdges
*
* Returns <allowDanglingEdges> as a boolean.
*/
mxGraph.prototype.isAllowDanglingEdges = function()
{
return this.allowDanglingEdges;
};
/**
* Function: setConnectableEdges
*
* Specifies if edges should be connectable.
*
* Parameters:
*
* value - Boolean indicating if edges should be connectable.
*/
mxGraph.prototype.setConnectableEdges = function(value)
{
this.connectableEdges = value;
};
/**
* Function: isConnectableEdges
*
* Returns <connectableEdges> as a boolean.
*/
mxGraph.prototype.isConnectableEdges = function()
{
return this.connectableEdges;
};
/**
* Function: setCloneInvalidEdges
*
* Specifies if edges should be inserted when cloned but not valid wrt.
* <getEdgeValidationError>. If false such edges will be silently ignored.
*
* Parameters:
*
* value - Boolean indicating if cloned invalid edges should be
* inserted into the graph or ignored.
*/
mxGraph.prototype.setCloneInvalidEdges = function(value)
{
this.cloneInvalidEdges = value;
};
/**
* Function: isCloneInvalidEdges
*
* Returns <cloneInvalidEdges> as a boolean.
*/
mxGraph.prototype.isCloneInvalidEdges = function()
{
return this.cloneInvalidEdges;
};
/**
* Function: setAllowLoops
*
* Specifies if loops are allowed.
*
* Parameters:
*
* value - Boolean indicating if loops are allowed.
*/
mxGraph.prototype.setAllowLoops = function(value)
{
this.allowLoops = value;
};
/**
* Function: isDisconnectOnMove
*
* Returns <disconnectOnMove> as a boolean.
*/
mxGraph.prototype.isDisconnectOnMove = function()
{
return this.disconnectOnMove;
};
/**
* Function: setDisconnectOnMove
*
* Specifies if edges should be disconnected when moved. (Note: Cloned
* edges are always disconnected.)
*
* Parameters:
*
* value - Boolean indicating if edges should be disconnected
* when moved.
*/
mxGraph.prototype.setDisconnectOnMove = function(value)
{
this.disconnectOnMove = value;
};
/**
* Function: isDropEnabled
*
* Returns <dropEnabled> as a boolean.
*/
mxGraph.prototype.isDropEnabled = function()
{
return this.dropEnabled;
};
/**
* Function: setDropEnabled
*
* Specifies if the graph should allow dropping of cells onto or into other
* cells.
*
* Parameters:
*
* dropEnabled - Boolean indicating if the graph should allow dropping
* of cells into other cells.
*/
mxGraph.prototype.setDropEnabled = function(value)
{
this.dropEnabled = value;
};
/**
* Function: isSplitEnabled
*
* Returns <splitEnabled> as a boolean.
*/
mxGraph.prototype.isSplitEnabled = function()
{
return this.splitEnabled;
};
/**
* Function: setSplitEnabled
*
* Specifies if the graph should allow dropping of cells onto or into other
* cells.
*
* Parameters:
*
* dropEnabled - Boolean indicating if the graph should allow dropping
* of cells into other cells.
*/
mxGraph.prototype.setSplitEnabled = function(value)
{
this.splitEnabled = value;
};
/**
* Function: isCellResizable
*
* Returns true if the given cell is resizable. This returns
* <cellsResizable> for all given cells if <isCellLocked> does not return
* true for the given cell and its style does not specify
* <mxConstants.STYLE_RESIZABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose resizable state should be returned.
*/
mxGraph.prototype.isCellResizable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsResizable() && !this.isCellLocked(cell) &&
mxUtils.getValue(style, mxConstants.STYLE_RESIZABLE, '1') != '0';
};
/**
* Function: isCellsResizable
*
* Returns <cellsResizable>.
*/
mxGraph.prototype.isCellsResizable = function()
{
return this.cellsResizable;
};
/**
* Function: setCellsResizable
*
* Specifies if the graph should allow resizing of cells. This
* implementation updates <cellsResizable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should allow resizing of
* cells.
*/
mxGraph.prototype.setCellsResizable = function(value)
{
this.cellsResizable = value;
};
/**
* Function: isTerminalPointMovable
*
* Returns true if the given terminal point is movable. This is independent
* from <isCellConnectable> and <isCellDisconnectable> and controls if terminal
* points can be moved in the graph if the edge is not connected. Note that it
* is required for this to return true to connect unconnected edges. This
* implementation returns true.
*
* Parameters:
*
* cell - <mxCell> whose terminal point should be moved.
* source - Boolean indicating if the source or target terminal should be moved.
*/
mxGraph.prototype.isTerminalPointMovable = function(cell, source)
{
return true;
};
/**
* Function: isCellBendable
*
* Returns true if the given cell is bendable. This returns <cellsBendable>
* for all given cells if <isLocked> does not return true for the given
* cell and its style does not specify <mxConstants.STYLE_BENDABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose bendable state should be returned.
*/
mxGraph.prototype.isCellBendable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsBendable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_BENDABLE] != 0;
};
/**
* Function: isCellsBendable
*
* Returns <cellsBenadable>.
*/
mxGraph.prototype.isCellsBendable = function()
{
return this.cellsBendable;
};
/**
* Function: setCellsBendable
*
* Specifies if the graph should allow bending of edges. This
* implementation updates <bendable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should allow bending of
* edges.
*/
mxGraph.prototype.setCellsBendable = function(value)
{
this.cellsBendable = value;
};
/**
* Function: isCellEditable
*
* Returns true if the given cell is editable. This returns <cellsEditable> for
* all given cells if <isCellLocked> does not return true for the given cell
* and its style does not specify <mxConstants.STYLE_EDITABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose editable state should be returned.
*/
mxGraph.prototype.isCellEditable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isCellsEditable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_EDITABLE] != 0;
};
/**
* Function: isCellsEditable
*
* Returns <cellsEditable>.
*/
mxGraph.prototype.isCellsEditable = function()
{
return this.cellsEditable;
};
/**
* Function: setCellsEditable
*
* Specifies if the graph should allow in-place editing for cell labels.
* This implementation updates <cellsEditable>.
*
* Parameters:
*
* value - Boolean indicating if the graph should allow in-place
* editing.
*/
mxGraph.prototype.setCellsEditable = function(value)
{
this.cellsEditable = value;
};
/**
* Function: isCellDisconnectable
*
* Returns true if the given cell is disconnectable from the source or
* target terminal. This returns <isCellsDisconnectable> for all given
* cells if <isCellLocked> does not return true for the given cell.
*
* Parameters:
*
* cell - <mxCell> whose disconnectable state should be returned.
* terminal - <mxCell> that represents the source or target terminal.
* source - Boolean indicating if the source or target terminal is to be
* disconnected.
*/
mxGraph.prototype.isCellDisconnectable = function(cell, terminal, source)
{
return this.isCellsDisconnectable() && !this.isCellLocked(cell);
};
/**
* Function: isCellsDisconnectable
*
* Returns <cellsDisconnectable>.
*/
mxGraph.prototype.isCellsDisconnectable = function()
{
return this.cellsDisconnectable;
};
/**
* Function: setCellsDisconnectable
*
* Sets <cellsDisconnectable>.
*/
mxGraph.prototype.setCellsDisconnectable = function(value)
{
this.cellsDisconnectable = value;
};
/**
* Function: isValidSource
*
* Returns true if the given cell is a valid source for new connections.
* This implementation returns true for all non-null values and is
* called by is called by <isValidConnection>.
*
* Parameters:
*
* cell - <mxCell> that represents a possible source or null.
*/
mxGraph.prototype.isValidSource = function(cell)
{
return (cell == null && this.allowDanglingEdges) ||
(cell != null && (!this.model.isEdge(cell) ||
this.connectableEdges) && this.isCellConnectable(cell));
};
/**
* Function: isValidTarget
*
* Returns <isValidSource> for the given cell. This is called by
* <isValidConnection>.
*
* Parameters:
*
* cell - <mxCell> that represents a possible target or null.
*/
mxGraph.prototype.isValidTarget = function(cell)
{
return this.isValidSource(cell);
};
/**
* Function: isValidConnection
*
* Returns true if the given target cell is a valid target for source.
* This is a boolean implementation for not allowing connections between
* certain pairs of vertices and is called by <getEdgeValidationError>.
* This implementation returns true if <isValidSource> returns true for
* the source and <isValidTarget> returns true for the target.
*
* Parameters:
*
* source - <mxCell> that represents the source cell.
* target - <mxCell> that represents the target cell.
*/
mxGraph.prototype.isValidConnection = function(source, target)
{
return this.isValidSource(source) && this.isValidTarget(target);
};
/**
* Function: setConnectable
*
* Specifies if the graph should allow new connections. This implementation
* updates <mxConnectionHandler.enabled> in <connectionHandler>.
*
* Parameters:
*
* connectable - Boolean indicating if new connections should be allowed.
*/
mxGraph.prototype.setConnectable = function(connectable)
{
this.connectionHandler.setEnabled(connectable);
};
/**
* Function: isConnectable
*
* Returns true if the <connectionHandler> is enabled.
*/
mxGraph.prototype.isConnectable = function()
{
return this.connectionHandler.isEnabled();
};
/**
* Function: setTooltips
*
* Specifies if tooltips should be enabled. This implementation updates
* <mxTooltipHandler.enabled> in <tooltipHandler>.
*
* Parameters:
*
* enabled - Boolean indicating if tooltips should be enabled.
*/
mxGraph.prototype.setTooltips = function (enabled)
{
this.tooltipHandler.setEnabled(enabled);
};
/**
* Function: setPanning
*
* Specifies if panning should be enabled. This implementation updates
* <mxPanningHandler.panningEnabled> in <panningHandler>.
*
* Parameters:
*
* enabled - Boolean indicating if panning should be enabled.
*/
mxGraph.prototype.setPanning = function(enabled)
{
this.panningHandler.panningEnabled = enabled;
};
/**
* Function: isEditing
*
* Returns true if the given cell is currently being edited.
* If no cell is specified then this returns true if any
* cell is currently being edited.
*
* Parameters:
*
* cell - <mxCell> that should be checked.
*/
mxGraph.prototype.isEditing = function(cell)
{
if (this.cellEditor != null)
{
var editingCell = this.cellEditor.getEditingCell();
return (cell == null) ? editingCell != null : cell == editingCell;
}
return false;
};
/**
* Function: isAutoSizeCell
*
* Returns true if the size of the given cell should automatically be
* updated after a change of the label. This implementation returns
* <autoSizeCells> or checks if the cell style does specify
* <mxConstants.STYLE_AUTOSIZE> to be 1.
*
* Parameters:
*
* cell - <mxCell> that should be resized.
*/
mxGraph.prototype.isAutoSizeCell = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.isAutoSizeCells() || style[mxConstants.STYLE_AUTOSIZE] == 1;
};
/**
* Function: isAutoSizeCells
*
* Returns <autoSizeCells>.
*/
mxGraph.prototype.isAutoSizeCells = function()
{
return this.autoSizeCells;
};
/**
* Function: setAutoSizeCells
*
* Specifies if cell sizes should be automatically updated after a label
* change. This implementation sets <autoSizeCells> to the given parameter.
* To update the size of cells when the cells are added, set
* <autoSizeCellsOnAdd> to true.
*
* Parameters:
*
* value - Boolean indicating if cells should be resized
* automatically.
*/
mxGraph.prototype.setAutoSizeCells = function(value)
{
this.autoSizeCells = value;
};
/**
* Function: isExtendParent
*
* Returns true if the parent of the given cell should be extended if the
* child has been resized so that it overlaps the parent. This
* implementation returns <isExtendParents> if the cell is not an edge.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
*/
mxGraph.prototype.isExtendParent = function(cell)
{
return !this.getModel().isEdge(cell) && this.isExtendParents();
};
/**
* Function: isExtendParents
*
* Returns <extendParents>.
*/
mxGraph.prototype.isExtendParents = function()
{
return this.extendParents;
};
/**
* Function: setExtendParents
*
* Sets <extendParents>.
*
* Parameters:
*
* value - New boolean value for <extendParents>.
*/
mxGraph.prototype.setExtendParents = function(value)
{
this.extendParents = value;
};
/**
* Function: isExtendParentsOnAdd
*
* Returns <extendParentsOnAdd>.
*/
mxGraph.prototype.isExtendParentsOnAdd = function(cell)
{
return this.extendParentsOnAdd;
};
/**
* Function: setExtendParentsOnAdd
*
* Sets <extendParentsOnAdd>.
*
* Parameters:
*
* value - New boolean value for <extendParentsOnAdd>.
*/
mxGraph.prototype.setExtendParentsOnAdd = function(value)
{
this.extendParentsOnAdd = value;
};
/**
* Function: isExtendParentsOnMove
*
* Returns <extendParentsOnMove>.
*/
mxGraph.prototype.isExtendParentsOnMove = function()
{
return this.extendParentsOnMove;
};
/**
* Function: setExtendParentsOnMove
*
* Sets <extendParentsOnMove>.
*
* Parameters:
*
* value - New boolean value for <extendParentsOnAdd>.
*/
mxGraph.prototype.setExtendParentsOnMove = function(value)
{
this.extendParentsOnMove = value;
};
/**
* Function: isRecursiveResize
*
* Returns <recursiveResize>.
*
* Parameters:
*
* state - <mxCellState> that is being resized.
*/
mxGraph.prototype.isRecursiveResize = function(state)
{
return this.recursiveResize;
};
/**
* Function: setRecursiveResize
*
* Sets <recursiveResize>.
*
* Parameters:
*
* value - New boolean value for <recursiveResize>.
*/
mxGraph.prototype.setRecursiveResize = function(value)
{
this.recursiveResize = value;
};
/**
* Function: isConstrainChild
*
* Returns true if the given cell should be kept inside the bounds of its
* parent according to the rules defined by <getOverlap> and
* <isAllowOverlapParent>. This implementation returns false for all children
* of edges and <isConstrainChildren> otherwise.
*
* Parameters:
*
* cell - <mxCell> that should be constrained.
*/
mxGraph.prototype.isConstrainChild = function(cell)
{
return this.isConstrainChildren() && !this.getModel().isEdge(this.getModel().getParent(cell));
};
/**
* Function: isConstrainChildren
*
* Returns <constrainChildren>.
*/
mxGraph.prototype.isConstrainChildren = function()
{
return this.constrainChildren;
};
/**
* Function: setConstrainChildren
*
* Sets <constrainChildren>.
*/
mxGraph.prototype.setConstrainChildren = function(value)
{
this.constrainChildren = value;
};
/**
* Function: isConstrainRelativeChildren
*
* Returns <constrainRelativeChildren>.
*/
mxGraph.prototype.isConstrainRelativeChildren = function()
{
return this.constrainRelativeChildren;
};
/**
* Function: setConstrainRelativeChildren
*
* Sets <constrainRelativeChildren>.
*/
mxGraph.prototype.setConstrainRelativeChildren = function(value)
{
this.constrainRelativeChildren = value;
};
/**
* Function: isConstrainChildren
*
* Returns <allowNegativeCoordinates>.
*/
mxGraph.prototype.isAllowNegativeCoordinates = function()
{
return this.allowNegativeCoordinates;
};
/**
* Function: setConstrainChildren
*
* Sets <allowNegativeCoordinates>.
*/
mxGraph.prototype.setAllowNegativeCoordinates = function(value)
{
this.allowNegativeCoordinates = value;
};
/**
* Function: getOverlap
*
* Returns a decimal number representing the amount of the width and height
* of the given cell that is allowed to overlap its parent. A value of 0
* means all children must stay inside the parent, 1 means the child is
* allowed to be placed outside of the parent such that it touches one of
* the parents sides. If <isAllowOverlapParent> returns false for the given
* cell, then this method returns 0.
*
* Parameters:
*
* cell - <mxCell> for which the overlap ratio should be returned.
*/
mxGraph.prototype.getOverlap = function(cell)
{
return (this.isAllowOverlapParent(cell)) ? this.defaultOverlap : 0;
};
/**
* Function: isAllowOverlapParent
*
* Returns true if the given cell is allowed to be placed outside of the
* parents area.
*
* Parameters:
*
* cell - <mxCell> that represents the child to be checked.
*/
mxGraph.prototype.isAllowOverlapParent = function(cell)
{
return false;
};
/**
* Function: getFoldableCells
*
* Returns the cells which are movable in the given array of cells.
*/
mxGraph.prototype.getFoldableCells = function(cells, collapse)
{
return this.model.filterCells(cells, mxUtils.bind(this, function(cell)
{
return this.isCellFoldable(cell, collapse);
}));
};
/**
* Function: isCellFoldable
*
* Returns true if the given cell is foldable. This implementation
* returns true if the cell has at least one child and its style
* does not specify <mxConstants.STYLE_FOLDABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose foldable state should be returned.
*/
mxGraph.prototype.isCellFoldable = function(cell, collapse)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return this.model.getChildCount(cell) > 0 && style[mxConstants.STYLE_FOLDABLE] != 0;
};
/**
* Function: isValidDropTarget
*
* Returns true if the given cell is a valid drop target for the specified
* cells. If <splitEnabled> is true then this returns <isSplitTarget> for
* the given arguments else it returns true if the cell is not collapsed
* and its child count is greater than 0.
*
* Parameters:
*
* cell - <mxCell> that represents the possible drop target.
* cells - <mxCells> that should be dropped into the target.
* evt - Mouseevent that triggered the invocation.
*/
mxGraph.prototype.isValidDropTarget = function(cell, cells, evt)
{
return cell != null && ((this.isSplitEnabled() &&
this.isSplitTarget(cell, cells, evt)) || (!this.model.isEdge(cell) &&
(this.isSwimlane(cell) || (this.model.getChildCount(cell) > 0 &&
!this.isCellCollapsed(cell)))));
};
/**
* Function: isSplitTarget
*
* Returns true if the given edge may be splitted into two edges with the
* given cell as a new terminal between the two.
*
* Parameters:
*
* target - <mxCell> that represents the edge to be splitted.
* cells - <mxCells> that should split the edge.
* evt - Mouseevent that triggered the invocation.
*/
mxGraph.prototype.isSplitTarget = function(target, cells, evt)
{
if (this.model.isEdge(target) && cells != null && cells.length == 1 &&
this.isCellConnectable(cells[0]) && this.getEdgeValidationError(target,
this.model.getTerminal(target, true), cells[0]) == null)
{
var src = this.model.getTerminal(target, true);
var trg = this.model.getTerminal(target, false);
return (!this.model.isAncestor(cells[0], src) &&
!this.model.isAncestor(cells[0], trg));
}
return false;
};
/**
* Function: getDropTarget
*
* Returns the given cell if it is a drop target for the given cells or the
* nearest ancestor that may be used as a drop target for the given cells.
* If the given array contains a swimlane and <swimlaneNesting> is false
* then this always returns null. If no cell is given, then the bottommost
* swimlane at the location of the given event is returned.
*
* This function should only be used if <isDropEnabled> returns true.
*
* Parameters:
*
* cells - Array of <mxCells> which are to be dropped onto the target.
* evt - Mouseevent for the drag and drop.
* cell - <mxCell> that is under the mousepointer.
* clone - Optional boolean to indicate of cells will be cloned.
*/
mxGraph.prototype.getDropTarget = function(cells, evt, cell, clone)
{
if (!this.isSwimlaneNesting())
{
for (var i = 0; i < cells.length; i++)
{
if (this.isSwimlane(cells[i]))
{
return null;
}
}
}
var pt = mxUtils.convertPoint(this.container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
pt.x -= this.panDx;
pt.y -= this.panDy;
var swimlane = this.getSwimlaneAt(pt.x, pt.y);
if (cell == null)
{
cell = swimlane;
}
else if (swimlane != null)
{
// Checks if the cell is an ancestor of the swimlane
// under the mouse and uses the swimlane in that case
var tmp = this.model.getParent(swimlane);
while (tmp != null && this.isSwimlane(tmp) && tmp != cell)
{
tmp = this.model.getParent(tmp);
}
if (tmp == cell)
{
cell = swimlane;
}
}
while (cell != null && !this.isValidDropTarget(cell, cells, evt) &&
!this.model.isLayer(cell))
{
cell = this.model.getParent(cell);
}
// Checks if parent is dropped into child if not cloning
if (clone == null || !clone)
{
var parent = cell;
while (parent != null && mxUtils.indexOf(cells, parent) < 0)
{
parent = this.model.getParent(parent);
}
}
return (!this.model.isLayer(cell) && parent == null) ? cell : null;
};
/**
* Group: Cell retrieval
*/
/**
* Function: getDefaultParent
*
* Returns <defaultParent> or <mxGraphView.currentRoot> or the first child
* child of <mxGraphModel.root> if both are null. The value returned by
* this function should be used as the parent for new cells (aka default
* layer).
*/
mxGraph.prototype.getDefaultParent = function()
{
var parent = this.getCurrentRoot();
if (parent == null)
{
parent = this.defaultParent;
if (parent == null)
{
var root = this.model.getRoot();
parent = this.model.getChildAt(root, 0);
}
}
return parent;
};
/**
* Function: setDefaultParent
*
* Sets the <defaultParent> to the given cell. Set this to null to return
* the first child of the root in getDefaultParent.
*/
mxGraph.prototype.setDefaultParent = function(cell)
{
this.defaultParent = cell;
};
/**
* Function: getSwimlane
*
* Returns the nearest ancestor of the given cell which is a swimlane, or
* the given cell, if it is itself a swimlane.
*
* Parameters:
*
* cell - <mxCell> for which the ancestor swimlane should be returned.
*/
mxGraph.prototype.getSwimlane = function(cell)
{
while (cell != null && !this.isSwimlane(cell))
{
cell = this.model.getParent(cell);
}
return cell;
};
/**
* Function: getSwimlaneAt
*
* Returns the bottom-most swimlane that intersects the given point (x, y)
* in the cell hierarchy that starts at the given parent.
*
* Parameters:
*
* x - X-coordinate of the location to be checked.
* y - Y-coordinate of the location to be checked.
* parent - <mxCell> that should be used as the root of the recursion.
* Default is <defaultParent>.
*/
mxGraph.prototype.getSwimlaneAt = function (x, y, parent)
{
parent = parent || this.getDefaultParent();
if (parent != null)
{
var childCount = this.model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = this.model.getChildAt(parent, i);
var result = this.getSwimlaneAt(x, y, child);
if (result != null)
{
return result;
}
else if (this.isSwimlane(child))
{
var state = this.view.getState(child);
if (this.intersects(state, x, y))
{
return child;
}
}
}
}
return null;
};
/**
* Function: getCellAt
*
* Returns the bottom-most cell that intersects the given point (x, y) in
* the cell hierarchy starting at the given parent. This will also return
* swimlanes if the given location intersects the content area of the
* swimlane. If this is not desired, then the <hitsSwimlaneContent> may be
* used if the returned cell is a swimlane to determine if the location
* is inside the content area or on the actual title of the swimlane.
*
* Parameters:
*
* x - X-coordinate of the location to be checked.
* y - Y-coordinate of the location to be checked.
* parent - <mxCell> that should be used as the root of the recursion.
* Default is current root of the view or the root of the model.
* vertices - Optional boolean indicating if vertices should be returned.
* Default is true.
* edges - Optional boolean indicating if edges should be returned. Default
* is true.
* ignoreFn - Optional function that returns true if cell should be ignored.
* The function is passed the cell state and the x and y parameter.
*/
mxGraph.prototype.getCellAt = function(x, y, parent, vertices, edges, ignoreFn)
{
vertices = (vertices != null) ? vertices : true;
edges = (edges != null) ? edges : true;
if (parent == null)
{
parent = this.getCurrentRoot();
if (parent == null)
{
parent = this.getModel().getRoot();
}
}
if (parent != null)
{
var childCount = this.model.getChildCount(parent);
for (var i = childCount - 1; i >= 0; i--)
{
var cell = this.model.getChildAt(parent, i);
var result = this.getCellAt(x, y, cell, vertices, edges, ignoreFn);
if (result != null)
{
return result;
}
else if (this.isCellVisible(cell) && (edges && this.model.isEdge(cell) ||
vertices && this.model.isVertex(cell)))
{
var state = this.view.getState(cell);
if (state != null && (ignoreFn == null || !ignoreFn(state, x, y)) &&
this.intersects(state, x, y))
{
return cell;
}
}
}
}
return null;
};
/**
* Function: intersects
*
* Returns the bottom-most cell that intersects the given point (x, y) in
* the cell hierarchy that starts at the given parent.
*
* Parameters:
*
* state - <mxCellState> that represents the cell state.
* x - X-coordinate of the location to be checked.
* y - Y-coordinate of the location to be checked.
*/
mxGraph.prototype.intersects = function(state, x, y)
{
if (state != null)
{
var pts = state.absolutePoints;
if (pts != null)
{
var t2 = this.tolerance * this.tolerance;
var pt = pts[0];
for (var i = 1; i < pts.length; i++)
{
var next = pts[i];
var dist = mxUtils.ptSegDistSq(pt.x, pt.y, next.x, next.y, x, y);
if (dist <= t2)
{
return true;
}
pt = next;
}
}
else
{
var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0);
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var cx = new mxPoint(state.getCenterX(), state.getCenterY());
var pt = mxUtils.getRotatedPoint(new mxPoint(x, y), cos, sin, cx);
x = pt.x;
y = pt.y;
}
if (mxUtils.contains(state, x, y))
{
return true;
}
}
}
return false;
};
/**
* Function: hitsSwimlaneContent
*
* Returns true if the given coordinate pair is inside the content
* are of the given swimlane.
*
* Parameters:
*
* swimlane - <mxCell> that specifies the swimlane.
* x - X-coordinate of the mouse event.
* y - Y-coordinate of the mouse event.
*/
mxGraph.prototype.hitsSwimlaneContent = function(swimlane, x, y)
{
var state = this.getView().getState(swimlane);
var size = this.getStartSize(swimlane);
if (state != null)
{
var scale = this.getView().getScale();
x -= state.x;
y -= state.y;
if (size.width > 0 && x > 0 && x > size.width * scale)
{
return true;
}
else if (size.height > 0 && y > 0 && y > size.height * scale)
{
return true;
}
}
return false;
};
/**
* Function: getChildVertices
*
* Returns the visible child vertices of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose children should be returned.
*/
mxGraph.prototype.getChildVertices = function(parent)
{
return this.getChildCells(parent, true, false);
};
/**
* Function: getChildEdges
*
* Returns the visible child edges of the given parent.
*
* Parameters:
*
* parent - <mxCell> whose child vertices should be returned.
*/
mxGraph.prototype.getChildEdges = function(parent)
{
return this.getChildCells(parent, false, true);
};
/**
* Function: getChildCells
*
* Returns the visible child vertices or edges in the given parent. If
* vertices and edges is false, then all children are returned.
*
* Parameters:
*
* parent - <mxCell> whose children should be returned.
* vertices - Optional boolean that specifies if child vertices should
* be returned. Default is false.
* edges - Optional boolean that specifies if child edges should
* be returned. Default is false.
*/
mxGraph.prototype.getChildCells = function(parent, vertices, edges)
{
parent = (parent != null) ? parent : this.getDefaultParent();
vertices = (vertices != null) ? vertices : false;
edges = (edges != null) ? edges : false;
var cells = this.model.getChildCells(parent, vertices, edges);
var result = [];
// Filters out the non-visible child cells
for (var i = 0; i < cells.length; i++)
{
if (this.isCellVisible(cells[i]))
{
result.push(cells[i]);
}
}
return result;
};
/**
* Function: getConnections
*
* Returns all visible edges connected to the given cell without loops.
*
* Parameters:
*
* cell - <mxCell> whose connections should be returned.
* parent - Optional parent of the opposite end for a connection to be
* returned.
*/
mxGraph.prototype.getConnections = function(cell, parent)
{
return this.getEdges(cell, parent, true, true, false);
};
/**
* Function: getIncomingEdges
*
* Returns the visible incoming edges for the given cell. If the optional
* parent argument is specified, then only child edges of the given parent
* are returned.
*
* Parameters:
*
* cell - <mxCell> whose incoming edges should be returned.
* parent - Optional parent of the opposite end for an edge to be
* returned.
*/
mxGraph.prototype.getIncomingEdges = function(cell, parent)
{
return this.getEdges(cell, parent, true, false, false);
};
/**
* Function: getOutgoingEdges
*
* Returns the visible outgoing edges for the given cell. If the optional
* parent argument is specified, then only child edges of the given parent
* are returned.
*
* Parameters:
*
* cell - <mxCell> whose outgoing edges should be returned.
* parent - Optional parent of the opposite end for an edge to be
* returned.
*/
mxGraph.prototype.getOutgoingEdges = function(cell, parent)
{
return this.getEdges(cell, parent, false, true, false);
};
/**
* Function: getEdges
*
* Returns the incoming and/or outgoing edges for the given cell.
* If the optional parent argument is specified, then only edges are returned
* where the opposite is in the given parent cell. If at least one of incoming
* or outgoing is true, then loops are ignored, if both are false, then all
* edges connected to the given cell are returned including loops.
*
* Parameters:
*
* cell - <mxCell> whose edges should be returned.
* parent - Optional parent of the opposite end for an edge to be
* returned.
* incoming - Optional boolean that specifies if incoming edges should
* be included in the result. Default is true.
* outgoing - Optional boolean that specifies if outgoing edges should
* be included in the result. Default is true.
* includeLoops - Optional boolean that specifies if loops should be
* included in the result. Default is true.
* recurse - Optional boolean the specifies if the parent specified only
* need be an ancestral parent, true, or the direct parent, false.
* Default is false
*/
mxGraph.prototype.getEdges = function(cell, parent, incoming, outgoing, includeLoops, recurse)
{
incoming = (incoming != null) ? incoming : true;
outgoing = (outgoing != null) ? outgoing : true;
includeLoops = (includeLoops != null) ? includeLoops : true;
recurse = (recurse != null) ? recurse : false;
var edges = [];
var isCollapsed = this.isCellCollapsed(cell);
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = this.model.getChildAt(cell, i);
if (isCollapsed || !this.isCellVisible(child))
{
edges = edges.concat(this.model.getEdges(child, incoming, outgoing));
}
}
edges = edges.concat(this.model.getEdges(cell, incoming, outgoing));
var result = [];
for (var i = 0; i < edges.length; i++)
{
var state = this.view.getState(edges[i]);
var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true);
var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false);
if ((includeLoops && source == target) || ((source != target) && ((incoming &&
target == cell && (parent == null || this.isValidAncestor(source, parent, recurse))) ||
(outgoing && source == cell && (parent == null ||
this.isValidAncestor(target, parent, recurse))))))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Function: isValidAncestor
*
* Returns whether or not the specified parent is a valid
* ancestor of the specified cell, either direct or indirectly
* based on whether ancestor recursion is enabled.
*
* Parameters:
*
* cell - <mxCell> the possible child cell
* parent - <mxCell> the possible parent cell
* recurse - boolean whether or not to recurse the child ancestors
*/
mxGraph.prototype.isValidAncestor = function(cell, parent, recurse)
{
return (recurse ? this.model.isAncestor(parent, cell) : this.model
.getParent(cell) == parent);
};
/**
* Function: getOpposites
*
* Returns all distinct visible opposite cells for the specified terminal
* on the given edges.
*
* Parameters:
*
* edges - Array of <mxCells> that contains the edges whose opposite
* terminals should be returned.
* terminal - Terminal that specifies the end whose opposite should be
* returned.
* source - Optional boolean that specifies if source terminals should be
* included in the result. Default is true.
* targets - Optional boolean that specifies if targer terminals should be
* included in the result. Default is true.
*/
mxGraph.prototype.getOpposites = function(edges, terminal, sources, targets)
{
sources = (sources != null) ? sources : true;
targets = (targets != null) ? targets : true;
var terminals = [];
// Fast lookup to avoid duplicates in terminals array
var dict = new mxDictionary();
if (edges != null)
{
for (var i = 0; i < edges.length; i++)
{
var state = this.view.getState(edges[i]);
var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true);
var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false);
// Checks if the terminal is the source of the edge and if the
// target should be stored in the result
if (source == terminal && target != null && target != terminal && targets)
{
if (!dict.get(target))
{
dict.put(target, true);
terminals.push(target);
}
}
// Checks if the terminal is the taget of the edge and if the
// source should be stored in the result
else if (target == terminal && source != null && source != terminal && sources)
{
if (!dict.get(source))
{
dict.put(source, true);
terminals.push(source);
}
}
}
}
return terminals;
};
/**
* Function: getEdgesBetween
*
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and returns the connected edges
* as displayed on the screen.
*
* Parameters:
*
* source -
* target -
* directed -
*/
mxGraph.prototype.getEdgesBetween = function(source, target, directed)
{
directed = (directed != null) ? directed : false;
var edges = this.getEdges(source);
var result = [];
// Checks if the edge is connected to the correct
// cell and returns the first match
for (var i = 0; i < edges.length; i++)
{
var state = this.view.getState(edges[i]);
var src = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true);
var trg = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target) || (!directed && src == target && trg == source))
{
result.push(edges[i]);
}
}
return result;
};
/**
* Function: getPointForEvent
*
* Returns an <mxPoint> representing the given event in the unscaled,
* non-translated coordinate space of <container> and applies the grid.
*
* Parameters:
*
* evt - Mousevent that contains the mouse pointer location.
* addOffset - Optional boolean that specifies if the position should be
* offset by half of the <gridSize>. Default is true.
*/
mxGraph.prototype.getPointForEvent = function(evt, addOffset)
{
var p = mxUtils.convertPoint(this.container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
var s = this.view.scale;
var tr = this.view.translate;
var off = (addOffset != false) ? this.gridSize / 2 : 0;
p.x = this.snap(p.x / s - tr.x - off);
p.y = this.snap(p.y / s - tr.y - off);
return p;
};
/**
* Function: getCells
*
* Returns the child vertices and edges of the given parent that are contained
* in the given rectangle. The result is added to the optional result array,
* which is returned. If no result array is specified then a new array is
* created and returned.
*
* Parameters:
*
* x - X-coordinate of the rectangle.
* y - Y-coordinate of the rectangle.
* width - Width of the rectangle.
* height - Height of the rectangle.
* parent - <mxCell> that should be used as the root of the recursion.
* Default is current root of the view or the root of the model.
* result - Optional array to store the result in.
*/
mxGraph.prototype.getCells = function(x, y, width, height, parent, result)
{
result = (result != null) ? result : [];
if (width > 0 || height > 0)
{
var model = this.getModel();
var right = x + width;
var bottom = y + height;
if (parent == null)
{
parent = this.getCurrentRoot();
if (parent == null)
{
parent = model.getRoot();
}
}
if (parent != null)
{
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
var state = this.view.getState(cell);
if (state != null && this.isCellVisible(cell))
{
var deg = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0;
var box = state;
if (deg != 0)
{
box = mxUtils.getBoundingBox(box, deg);
}
if ((model.isEdge(cell) || model.isVertex(cell)) &&
box.x >= x && box.y + box.height <= bottom &&
box.y >= y && box.x + box.width <= right)
{
result.push(cell);
}
else
{
this.getCells(x, y, width, height, cell, result);
}
}
}
}
}
return result;
};
/**
* Function: getCellsBeyond
*
* Returns the children of the given parent that are contained in the
* halfpane from the given point (x0, y0) rightwards or downwards
* depending on rightHalfpane and bottomHalfpane.
*
* Parameters:
*
* x0 - X-coordinate of the origin.
* y0 - Y-coordinate of the origin.
* parent - Optional <mxCell> whose children should be checked. Default is
* <defaultParent>.
* rightHalfpane - Boolean indicating if the cells in the right halfpane
* from the origin should be returned.
* bottomHalfpane - Boolean indicating if the cells in the bottom halfpane
* from the origin should be returned.
*/
mxGraph.prototype.getCellsBeyond = function(x0, y0, parent, rightHalfpane, bottomHalfpane)
{
var result = [];
if (rightHalfpane || bottomHalfpane)
{
if (parent == null)
{
parent = this.getDefaultParent();
}
if (parent != null)
{
var childCount = this.model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var child = this.model.getChildAt(parent, i);
var state = this.view.getState(child);
if (this.isCellVisible(child) && state != null)
{
if ((!rightHalfpane || state.x >= x0) &&
(!bottomHalfpane || state.y >= y0))
{
result.push(child);
}
}
}
}
}
return result;
};
/**
* Function: findTreeRoots
*
* Returns all children in the given parent which do not have incoming
* edges. If the result is empty then the with the greatest difference
* between incoming and outgoing edges is returned.
*
* Parameters:
*
* parent - <mxCell> whose children should be checked.
* isolate - Optional boolean that specifies if edges should be ignored if
* the opposite end is not a child of the given parent cell. Default is
* false.
* invert - Optional boolean that specifies if outgoing or incoming edges
* should be counted for a tree root. If false then outgoing edges will be
* counted. Default is false.
*/
mxGraph.prototype.findTreeRoots = function(parent, isolate, invert)
{
isolate = (isolate != null) ? isolate : false;
invert = (invert != null) ? invert : false;
var roots = [];
if (parent != null)
{
var model = this.getModel();
var childCount = model.getChildCount(parent);
var best = null;
var maxDiff = 0;
for (var i=0; i<childCount; i++)
{
var cell = model.getChildAt(parent, i);
if (this.model.isVertex(cell) && this.isCellVisible(cell))
{
var conns = this.getConnections(cell, (isolate) ? parent : null);
var fanOut = 0;
var fanIn = 0;
for (var j = 0; j < conns.length; j++)
{
var src = this.view.getVisibleTerminal(conns[j], true);
if (src == cell)
{
fanOut++;
}
else
{
fanIn++;
}
}
if ((invert && fanOut == 0 && fanIn > 0) ||
(!invert && fanIn == 0 && fanOut > 0))
{
roots.push(cell);
}
var diff = (invert) ? fanIn - fanOut : fanOut - fanIn;
if (diff > maxDiff)
{
maxDiff = diff;
best = cell;
}
}
}
if (roots.length == 0 && best != null)
{
roots.push(best);
}
}
return roots;
};
/**
* Function: traverse
*
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* Example:
*
* (code)
* mxLog.show();
* var cell = graph.getSelectionCell();
* graph.traverse(cell, false, function(vertex, edge)
* {
* mxLog.debug(graph.getLabel(vertex));
* });
* (end)
*
* Parameters:
*
* vertex - <mxCell> that represents the vertex where the traversal starts.
* directed - Optional boolean indicating if edges should only be traversed
* from source to target. Default is true.
* func - Visitor function that takes the current vertex and the incoming
* edge as arguments. The traversal stops if the function returns false.
* edge - Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* visited - Optional <mxDictionary> from cells to true for the visited cells.
* inverse - Optional boolean to traverse in inverse direction. Default is false.
* This is ignored if directed is false.
*/
mxGraph.prototype.traverse = function(vertex, directed, func, edge, visited, inverse)
{
if (func != null && vertex != null)
{
directed = (directed != null) ? directed : true;
inverse = (inverse != null) ? inverse : false;
visited = visited || new mxDictionary();
if (!visited.get(vertex))
{
visited.put(vertex, true);
var result = func(vertex, edge);
if (result == null || result)
{
var edgeCount = this.model.getEdgeCount(vertex);
if (edgeCount > 0)
{
for (var i = 0; i < edgeCount; i++)
{
var e = this.model.getEdgeAt(vertex, i);
var isSource = this.model.getTerminal(e, true) == vertex;
if (!directed || (!inverse == isSource))
{
var next = this.model.getTerminal(e, !isSource);
this.traverse(next, directed, func, e, visited, inverse);
}
}
}
}
}
}
};
/**
* Group: Selection
*/
/**
* Function: isCellSelected
*
* Returns true if the given cell is selected.
*
* Parameters:
*
* cell - <mxCell> for which the selection state should be returned.
*/
mxGraph.prototype.isCellSelected = function(cell)
{
return this.getSelectionModel().isSelected(cell);
};
/**
* Function: isSelectionEmpty
*
* Returns true if the selection is empty.
*/
mxGraph.prototype.isSelectionEmpty = function()
{
return this.getSelectionModel().isEmpty();
};
/**
* Function: clearSelection
*
* Clears the selection using <mxGraphSelectionModel.clear>.
*/
mxGraph.prototype.clearSelection = function()
{
return this.getSelectionModel().clear();
};
/**
* Function: getSelectionCount
*
* Returns the number of selected cells.
*/
mxGraph.prototype.getSelectionCount = function()
{
return this.getSelectionModel().cells.length;
};
/**
* Function: getSelectionCell
*
* Returns the first cell from the array of selected <mxCells>.
*/
mxGraph.prototype.getSelectionCell = function()
{
return this.getSelectionModel().cells[0];
};
/**
* Function: getSelectionCells
*
* Returns the array of selected <mxCells>.
*/
mxGraph.prototype.getSelectionCells = function()
{
return this.getSelectionModel().cells.slice();
};
/**
* Function: setSelectionCell
*
* Sets the selection cell.
*
* Parameters:
*
* cell - <mxCell> to be selected.
*/
mxGraph.prototype.setSelectionCell = function(cell)
{
this.getSelectionModel().setCell(cell);
};
/**
* Function: setSelectionCells
*
* Sets the selection cell.
*
* Parameters:
*
* cells - Array of <mxCells> to be selected.
*/
mxGraph.prototype.setSelectionCells = function(cells)
{
this.getSelectionModel().setCells(cells);
};
/**
* Function: addSelectionCell
*
* Adds the given cell to the selection.
*
* Parameters:
*
* cell - <mxCell> to be add to the selection.
*/
mxGraph.prototype.addSelectionCell = function(cell)
{
this.getSelectionModel().addCell(cell);
};
/**
* Function: addSelectionCells
*
* Adds the given cells to the selection.
*
* Parameters:
*
* cells - Array of <mxCells> to be added to the selection.
*/
mxGraph.prototype.addSelectionCells = function(cells)
{
this.getSelectionModel().addCells(cells);
};
/**
* Function: removeSelectionCell
*
* Removes the given cell from the selection.
*
* Parameters:
*
* cell - <mxCell> to be removed from the selection.
*/
mxGraph.prototype.removeSelectionCell = function(cell)
{
this.getSelectionModel().removeCell(cell);
};
/**
* Function: removeSelectionCells
*
* Removes the given cells from the selection.
*
* Parameters:
*
* cells - Array of <mxCells> to be removed from the selection.
*/
mxGraph.prototype.removeSelectionCells = function(cells)
{
this.getSelectionModel().removeCells(cells);
};
/**
* Function: selectRegion
*
* Selects and returns the cells inside the given rectangle for the
* specified event.
*
* Parameters:
*
* rect - <mxRectangle> that represents the region to be selected.
* evt - Mouseevent that triggered the selection.
*/
mxGraph.prototype.selectRegion = function(rect, evt)
{
var cells = this.getCells(rect.x, rect.y, rect.width, rect.height);
this.selectCellsForEvent(cells, evt);
return cells;
};
/**
* Function: selectNextCell
*
* Selects the next cell.
*/
mxGraph.prototype.selectNextCell = function()
{
this.selectCell(true);
};
/**
* Function: selectPreviousCell
*
* Selects the previous cell.
*/
mxGraph.prototype.selectPreviousCell = function()
{
this.selectCell();
};
/**
* Function: selectParentCell
*
* Selects the parent cell.
*/
mxGraph.prototype.selectParentCell = function()
{
this.selectCell(false, true);
};
/**
* Function: selectChildCell
*
* Selects the first child cell.
*/
mxGraph.prototype.selectChildCell = function()
{
this.selectCell(false, false, true);
};
/**
* Function: selectCell
*
* Selects the next, parent, first child or previous cell, if all arguments
* are false.
*
* Parameters:
*
* isNext - Boolean indicating if the next cell should be selected.
* isParent - Boolean indicating if the parent cell should be selected.
* isChild - Boolean indicating if the first child cell should be selected.
*/
mxGraph.prototype.selectCell = function(isNext, isParent, isChild)
{
var sel = this.selectionModel;
var cell = (sel.cells.length > 0) ? sel.cells[0] : null;
if (sel.cells.length > 1)
{
sel.clear();
}
var parent = (cell != null) ?
this.model.getParent(cell) :
this.getDefaultParent();
var childCount = this.model.getChildCount(parent);
if (cell == null && childCount > 0)
{
var child = this.model.getChildAt(parent, 0);
this.setSelectionCell(child);
}
else if ((cell == null || isParent) &&
this.view.getState(parent) != null &&
this.model.getGeometry(parent) != null)
{
if (this.getCurrentRoot() != parent)
{
this.setSelectionCell(parent);
}
}
else if (cell != null && isChild)
{
var tmp = this.model.getChildCount(cell);
if (tmp > 0)
{
var child = this.model.getChildAt(cell, 0);
this.setSelectionCell(child);
}
}
else if (childCount > 0)
{
var i = parent.getIndex(cell);
if (isNext)
{
i++;
var child = this.model.getChildAt(parent, i % childCount);
this.setSelectionCell(child);
}
else
{
i--;
var index = (i < 0) ? childCount - 1 : i;
var child = this.model.getChildAt(parent, index);
this.setSelectionCell(child);
}
}
};
/**
* Function: selectAll
*
* Selects all children of the given parent cell or the children of the
* default parent if no parent is specified. To select leaf vertices and/or
* edges use <selectCells>.
*
* Parameters:
*
* parent - Optional <mxCell> whose children should be selected.
* Default is <defaultParent>.
* descendants - Optional boolean specifying whether all descendants should be
* selected. Default is false.
*/
mxGraph.prototype.selectAll = function(parent, descendants)
{
parent = parent || this.getDefaultParent();
var cells = (descendants) ? this.model.filterDescendants(mxUtils.bind(this, function(cell)
{
return cell != parent && this.view.getState(cell) != null;
}), parent) : this.model.getChildren(parent);
if (cells != null)
{
this.setSelectionCells(cells);
}
};
/**
* Function: selectVertices
*
* Select all vertices inside the given parent or the default parent.
*/
mxGraph.prototype.selectVertices = function(parent)
{
this.selectCells(true, false, parent);
};
/**
* Function: selectVertices
*
* Select all vertices inside the given parent or the default parent.
*/
mxGraph.prototype.selectEdges = function(parent)
{
this.selectCells(false, true, parent);
};
/**
* Function: selectCells
*
* Selects all vertices and/or edges depending on the given boolean
* arguments recursively, starting at the given parent or the default
* parent if no parent is specified. Use <selectAll> to select all cells.
* For vertices, only cells with no children are selected.
*
* Parameters:
*
* vertices - Boolean indicating if vertices should be selected.
* edges - Boolean indicating if edges should be selected.
* parent - Optional <mxCell> that acts as the root of the recursion.
* Default is <defaultParent>.
*/
mxGraph.prototype.selectCells = function(vertices, edges, parent)
{
parent = parent || this.getDefaultParent();
var filter = mxUtils.bind(this, function(cell)
{
return this.view.getState(cell) != null &&
((this.model.getChildCount(cell) == 0 && this.model.isVertex(cell) && vertices
&& !this.model.isEdge(this.model.getParent(cell))) ||
(this.model.isEdge(cell) && edges));
});
var cells = this.model.filterDescendants(filter, parent);
if (cells != null)
{
this.setSelectionCells(cells);
}
};
/**
* Function: selectCellForEvent
*
* Selects the given cell by either adding it to the selection or
* replacing the selection depending on whether the given mouse event is a
* toggle event.
*
* Parameters:
*
* cell - <mxCell> to be selected.
* evt - Optional mouseevent that triggered the selection.
*/
mxGraph.prototype.selectCellForEvent = function(cell, evt)
{
var isSelected = this.isCellSelected(cell);
if (this.isToggleEvent(evt))
{
if (isSelected)
{
this.removeSelectionCell(cell);
}
else
{
this.addSelectionCell(cell);
}
}
else if (!isSelected || this.getSelectionCount() != 1)
{
this.setSelectionCell(cell);
}
};
/**
* Function: selectCellsForEvent
*
* Selects the given cells by either adding them to the selection or
* replacing the selection depending on whether the given mouse event is a
* toggle event.
*
* Parameters:
*
* cells - Array of <mxCells> to be selected.
* evt - Optional mouseevent that triggered the selection.
*/
mxGraph.prototype.selectCellsForEvent = function(cells, evt)
{
if (this.isToggleEvent(evt))
{
this.addSelectionCells(cells);
}
else
{
this.setSelectionCells(cells);
}
};
/**
* Group: Selection state
*/
/**
* Function: createHandler
*
* Creates a new handler for the given cell state. This implementation
* returns a new <mxEdgeHandler> of the corresponding cell is an edge,
* otherwise it returns an <mxVertexHandler>.
*
* Parameters:
*
* state - <mxCellState> whose handler should be created.
*/
mxGraph.prototype.createHandler = function(state)
{
var result = null;
if (state != null)
{
if (this.model.isEdge(state.cell))
{
var source = state.getVisibleTerminalState(true);
var target = state.getVisibleTerminalState(false);
var geo = this.getCellGeometry(state.cell);
var edgeStyle = this.view.getEdgeStyle(state, (geo != null) ? geo.points : null, source, target);
result = this.createEdgeHandler(state, edgeStyle);
}
else
{
result = this.createVertexHandler(state);
}
}
return result;
};
/**
* Function: createVertexHandler
*
* Hooks to create a new <mxVertexHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createVertexHandler = function(state)
{
return new mxVertexHandler(state);
};
/**
* Function: createEdgeHandler
*
* Hooks to create a new <mxEdgeHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createEdgeHandler = function(state, edgeStyle)
{
var result = null;
if (edgeStyle == mxEdgeStyle.Loop ||
edgeStyle == mxEdgeStyle.ElbowConnector ||
edgeStyle == mxEdgeStyle.SideToSide ||
edgeStyle == mxEdgeStyle.TopToBottom)
{
result = this.createElbowEdgeHandler(state);
}
else if (edgeStyle == mxEdgeStyle.SegmentConnector ||
edgeStyle == mxEdgeStyle.OrthConnector)
{
result = this.createEdgeSegmentHandler(state);
}
else
{
result = new mxEdgeHandler(state);
}
return result;
};
/**
* Function: createEdgeSegmentHandler
*
* Hooks to create a new <mxEdgeSegmentHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createEdgeSegmentHandler = function(state)
{
return new mxEdgeSegmentHandler(state);
};
/**
* Function: createElbowEdgeHandler
*
* Hooks to create a new <mxElbowEdgeHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createElbowEdgeHandler = function(state)
{
return new mxElbowEdgeHandler(state);
};
/**
* Group: Graph events
*/
/**
* Function: addMouseListener
*
* Adds a listener to the graph event dispatch loop. The listener
* must implement the mouseDown, mouseMove and mouseUp methods
* as shown in the <mxMouseEvent> class.
*
* Parameters:
*
* listener - Listener to be added to the graph event listeners.
*/
mxGraph.prototype.addMouseListener = function(listener)
{
if (this.mouseListeners == null)
{
this.mouseListeners = [];
}
this.mouseListeners.push(listener);
};
/**
* Function: removeMouseListener
*
* Removes the specified graph listener.
*
* Parameters:
*
* listener - Listener to be removed from the graph event listeners.
*/
mxGraph.prototype.removeMouseListener = function(listener)
{
if (this.mouseListeners != null)
{
for (var i = 0; i < this.mouseListeners.length; i++)
{
if (this.mouseListeners[i] == listener)
{
this.mouseListeners.splice(i, 1);
break;
}
}
}
};
/**
* Function: updateMouseEvent
*
* Sets the graphX and graphY properties if the given <mxMouseEvent> if
* required and returned the event.
*
* Parameters:
*
* me - <mxMouseEvent> to be updated.
* evtName - Name of the mouse event.
*/
mxGraph.prototype.updateMouseEvent = function(me, evtName)
{
if (me.graphX == null || me.graphY == null)
{
var pt = mxUtils.convertPoint(this.container, me.getX(), me.getY());
me.graphX = pt.x - this.panDx;
me.graphY = pt.y - this.panDy;
// Searches for rectangles using method if native hit detection is disabled on shape
if (me.getCell() == null && this.isMouseDown && evtName == mxEvent.MOUSE_MOVE)
{
me.state = this.view.getState(this.getCellAt(pt.x, pt.y, null, null, null, function(state)
{
return state.shape == null || state.shape.paintBackground != mxRectangleShape.prototype.paintBackground ||
mxUtils.getValue(state.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1' ||
(state.shape.fill != null && state.shape.fill != mxConstants.NONE);
}));
}
}
return me;
};
/**
* Function: getStateForEvent
*
* Returns the state for the given touch event.
*/
mxGraph.prototype.getStateForTouchEvent = function(evt)
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
// Dispatches the drop event to the graph which
// consumes and executes the source function
var pt = mxUtils.convertPoint(this.container, x, y);
return this.view.getState(this.getCellAt(pt.x, pt.y));
};
/**
* Function: isEventIgnored
*
* Returns true if the event should be ignored in <fireMouseEvent>.
*/
mxGraph.prototype.isEventIgnored = function(evtName, me, sender)
{
var mouseEvent = mxEvent.isMouseEvent(me.getEvent());
var result = false;
// Drops events that are fired more than once
if (me.getEvent() == this.lastEvent)
{
result = true;
}
else
{
this.lastEvent = me.getEvent();
}
// Installs event listeners to capture the complete gesture from the event source
// for non-MS touch events as a workaround for all events for the same geture being
// fired from the event source even if that was removed from the DOM.
if (this.eventSource != null && evtName != mxEvent.MOUSE_MOVE)
{
mxEvent.removeGestureListeners(this.eventSource, null, this.mouseMoveRedirect, this.mouseUpRedirect);
this.mouseMoveRedirect = null;
this.mouseUpRedirect = null;
this.eventSource = null;
}
else if (!mxClient.IS_GC && this.eventSource != null && me.getSource() != this.eventSource)
{
result = true;
}
else if (mxClient.IS_TOUCH && evtName == mxEvent.MOUSE_DOWN && !mouseEvent && !mxEvent.isPenEvent(me.getEvent()))
{
this.eventSource = me.getSource();
this.mouseMoveRedirect = mxUtils.bind(this, function(evt)
{
this.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, this.getStateForTouchEvent(evt)));
});
this.mouseUpRedirect = mxUtils.bind(this, function(evt)
{
this.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, this.getStateForTouchEvent(evt)));
});
mxEvent.addGestureListeners(this.eventSource, null, this.mouseMoveRedirect, this.mouseUpRedirect);
}
// Factored out the workarounds for FF to make it easier to override/remove
// Note this method has side-effects!
if (this.isSyntheticEventIgnored(evtName, me, sender))
{
result = true;
}
// Never fires mouseUp/-Down for double clicks
if (!mxEvent.isPopupTrigger(this.lastEvent) && evtName != mxEvent.MOUSE_MOVE && this.lastEvent.detail == 2)
{
return true;
}
// Filters out of sequence events or mixed event types during a gesture
if (evtName == mxEvent.MOUSE_UP && this.isMouseDown)
{
this.isMouseDown = false;
}
else if (evtName == mxEvent.MOUSE_DOWN && !this.isMouseDown)
{
this.isMouseDown = true;
this.isMouseTrigger = mouseEvent;
}
// Drops mouse events that are fired during touch gestures as a workaround for Webkit
// and mouse events that are not in sync with the current internal button state
else if (!result && (((!mxClient.IS_FF || evtName != mxEvent.MOUSE_MOVE) &&
this.isMouseDown && this.isMouseTrigger != mouseEvent) ||
(evtName == mxEvent.MOUSE_DOWN && this.isMouseDown) ||
(evtName == mxEvent.MOUSE_UP && !this.isMouseDown)))
{
result = true;
}
if (!result && evtName == mxEvent.MOUSE_DOWN)
{
this.lastMouseX = me.getX();
this.lastMouseY = me.getY();
}
return result;
};
/**
* Function: isSyntheticEventIgnored
*
* Hook for ignoring synthetic mouse events after touchend in Firefox.
*/
mxGraph.prototype.isSyntheticEventIgnored = function(evtName, me, sender)
{
var result = false;
var mouseEvent = mxEvent.isMouseEvent(me.getEvent());
// LATER: This does not cover all possible cases that can go wrong in FF
if (this.ignoreMouseEvents && mouseEvent && evtName != mxEvent.MOUSE_MOVE)
{
this.ignoreMouseEvents = evtName != mxEvent.MOUSE_UP;
result = true;
}
else if (mxClient.IS_FF && !mouseEvent && evtName == mxEvent.MOUSE_UP)
{
this.ignoreMouseEvents = true;
}
return result;
};
/**
* Function: isEventSourceIgnored
*
* Returns true if the event should be ignored in <fireMouseEvent>. This
* implementation returns true for select, option and input (if not of type
* checkbox, radio, button, submit or file) event sources if the event is not
* a mouse event or a left mouse button press event.
*
* Parameters:
*
* evtName - The name of the event.
* me - <mxMouseEvent> that should be ignored.
*/
mxGraph.prototype.isEventSourceIgnored = function(evtName, me)
{
var source = me.getSource();
var name = (source.nodeName != null) ? source.nodeName.toLowerCase() : '';
var candidate = !mxEvent.isMouseEvent(me.getEvent()) || mxEvent.isLeftMouseButton(me.getEvent());
return evtName == mxEvent.MOUSE_DOWN && candidate && (name == 'select' || name == 'option' ||
(name == 'input' && source.type != 'checkbox' && source.type != 'radio' &&
source.type != 'button' && source.type != 'submit' && source.type != 'file'));
};
/**
* Function: getEventState
*
* Returns the <mxCellState> to be used when firing the mouse event for the
* given state. This implementation returns the given state.
*
* Parameters:
*
* <mxCellState> - State whose event source should be returned.
*/
mxGraph.prototype.getEventState = function(state)
{
return state;
};
/**
* Function: fireMouseEvent
*
* Dispatches the given event in the graph event dispatch loop. Possible
* event names are <mxEvent.MOUSE_DOWN>, <mxEvent.MOUSE_MOVE> and
* <mxEvent.MOUSE_UP>. All listeners are invoked for all events regardless
* of the consumed state of the event.
*
* Parameters:
*
* evtName - String that specifies the type of event to be dispatched.
* me - <mxMouseEvent> to be fired.
* sender - Optional sender argument. Default is this.
*/
mxGraph.prototype.fireMouseEvent = function(evtName, me, sender)
{
if (this.isEventSourceIgnored(evtName, me))
{
if (this.tooltipHandler != null)
{
this.tooltipHandler.hide();
}
return;
}
if (sender == null)
{
sender = this;
}
// Updates the graph coordinates in the event
me = this.updateMouseEvent(me, evtName);
// Detects and processes double taps for touch-based devices which do not have native double click events
// or where detection of double click is not always possible (quirks, IE10+). Note that this can only handle
// double clicks on cells because the sequence of events in IE prevents detection on the background, it fires
// two mouse ups, one of which without a cell but no mousedown for the second click which means we cannot
// detect which mouseup(s) are part of the first click, ie we do not know when the first click ends.
if ((!this.nativeDblClickEnabled && !mxEvent.isPopupTrigger(me.getEvent())) || (this.doubleTapEnabled &&
mxClient.IS_TOUCH && (mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent()))))
{
var currentTime = new Date().getTime();
// NOTE: Second mouseDown for double click missing in quirks mode
if ((!mxClient.IS_QUIRKS && evtName == mxEvent.MOUSE_DOWN) || (mxClient.IS_QUIRKS && evtName == mxEvent.MOUSE_UP && !this.fireDoubleClick))
{
if (this.lastTouchEvent != null && this.lastTouchEvent != me.getEvent() &&
currentTime - this.lastTouchTime < this.doubleTapTimeout &&
Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance &&
Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance &&
this.doubleClickCounter < 2)
{
this.doubleClickCounter++;
var doubleClickFired = false;
if (evtName == mxEvent.MOUSE_UP)
{
if (me.getCell() == this.lastTouchCell && this.lastTouchCell != null)
{
this.lastTouchTime = 0;
var cell = this.lastTouchCell;
this.lastTouchCell = null;
// Fires native dblclick event via event source
// NOTE: This fires two double click events on edges in quirks mode. While
// trying to fix this, we realized that nativeDoubleClick can be disabled for
// quirks and IE10+ (or we didn't find the case mentioned above where it
// would not work), ie. all double clicks seem to be working without this.
if (mxClient.IS_QUIRKS)
{
me.getSource().fireEvent('ondblclick');
}
this.dblClick(me.getEvent(), cell);
doubleClickFired = true;
}
}
else
{
this.fireDoubleClick = true;
this.lastTouchTime = 0;
}
// Do not ignore mouse up in quirks in this case
if (!mxClient.IS_QUIRKS || doubleClickFired)
{
mxEvent.consume(me.getEvent());
return;
}
}
else if (this.lastTouchEvent == null || this.lastTouchEvent != me.getEvent())
{
this.lastTouchCell = me.getCell();
this.lastTouchX = me.getX();
this.lastTouchY = me.getY();
this.lastTouchTime = currentTime;
this.lastTouchEvent = me.getEvent();
this.doubleClickCounter = 0;
}
}
else if ((this.isMouseDown || evtName == mxEvent.MOUSE_UP) && this.fireDoubleClick)
{
this.fireDoubleClick = false;
var cell = this.lastTouchCell;
this.lastTouchCell = null;
this.isMouseDown = false;
// Workaround for Chrome/Safari not firing native double click events for double touch on background
var valid = (cell != null) || ((mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent())) &&
(mxClient.IS_GC || mxClient.IS_SF));
if (valid && Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance &&
Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance)
{
this.dblClick(me.getEvent(), cell);
}
else
{
mxEvent.consume(me.getEvent());
}
return;
}
}
if (!this.isEventIgnored(evtName, me, sender))
{
// Updates the event state via getEventState
me.state = this.getEventState(me.getState());
this.fireEvent(new mxEventObject(mxEvent.FIRE_MOUSE_EVENT, 'eventName', evtName, 'event', me));
if ((mxClient.IS_OP || mxClient.IS_SF || mxClient.IS_GC || mxClient.IS_IE11 ||
(mxClient.IS_IE && mxClient.IS_SVG) || me.getEvent().target != this.container))
{
if (evtName == mxEvent.MOUSE_MOVE && this.isMouseDown && this.autoScroll && !mxEvent.isMultiTouchEvent(me.getEvent))
{
this.scrollPointToVisible(me.getGraphX(), me.getGraphY(), this.autoExtend);
}
else if (evtName == mxEvent.MOUSE_UP && this.ignoreScrollbars && this.translateToScrollPosition &&
(this.container.scrollLeft != 0 || this.container.scrollTop != 0))
{
var s = this.view.scale;
var tr = this.view.translate;
this.view.setTranslate(tr.x - this.container.scrollLeft / s, tr.y - this.container.scrollTop / s);
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
}
if (this.mouseListeners != null)
{
var args = [sender, me];
// Does not change returnValue in Opera
if (!me.getEvent().preventDefault)
{
me.getEvent().returnValue = true;
}
for (var i = 0; i < this.mouseListeners.length; i++)
{
var l = this.mouseListeners[i];
if (evtName == mxEvent.MOUSE_DOWN)
{
l.mouseDown.apply(l, args);
}
else if (evtName == mxEvent.MOUSE_MOVE)
{
l.mouseMove.apply(l, args);
}
else if (evtName == mxEvent.MOUSE_UP)
{
l.mouseUp.apply(l, args);
}
}
}
// Invokes the click handler
if (evtName == mxEvent.MOUSE_UP)
{
this.click(me);
}
}
// Detects tapAndHold events using a timer
if ((mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent())) &&
evtName == mxEvent.MOUSE_DOWN && this.tapAndHoldEnabled && !this.tapAndHoldInProgress)
{
this.tapAndHoldInProgress = true;
this.initialTouchX = me.getGraphX();
this.initialTouchY = me.getGraphY();
var handler = function()
{
if (this.tapAndHoldValid)
{
this.tapAndHold(me);
}
this.tapAndHoldInProgress = false;
this.tapAndHoldValid = false;
};
if (this.tapAndHoldThread)
{
window.clearTimeout(this.tapAndHoldThread);
}
this.tapAndHoldThread = window.setTimeout(mxUtils.bind(this, handler), this.tapAndHoldDelay);
this.tapAndHoldValid = true;
}
else if (evtName == mxEvent.MOUSE_UP)
{
this.tapAndHoldInProgress = false;
this.tapAndHoldValid = false;
}
else if (this.tapAndHoldValid)
{
this.tapAndHoldValid =
Math.abs(this.initialTouchX - me.getGraphX()) < this.tolerance &&
Math.abs(this.initialTouchY - me.getGraphY()) < this.tolerance;
}
// Stops editing for all events other than from cellEditor
if (evtName == mxEvent.MOUSE_DOWN && this.isEditing() && !this.cellEditor.isEventSource(me.getEvent()))
{
this.stopEditing(!this.isInvokesStopCellEditing());
}
this.consumeMouseEvent(evtName, me, sender);
}
};
/**
* Function: consumeMouseEvent
*
* Consumes the given <mxMouseEvent> if it's a touchStart event.
*/
mxGraph.prototype.consumeMouseEvent = function(evtName, me, sender)
{
// Workaround for duplicate click in Windows 8 with Chrome/FF/Opera with touch
if (evtName == mxEvent.MOUSE_DOWN && mxEvent.isTouchEvent(me.getEvent()))
{
me.consume(false);
}
};
/**
* Function: fireGestureEvent
*
* Dispatches a <mxEvent.GESTURE> event. The following example will resize the
* cell under the mouse based on the scale property of the native touch event.
*
* (code)
* graph.addListener(mxEvent.GESTURE, function(sender, eo)
* {
* var evt = eo.getProperty('event');
* var state = graph.view.getState(eo.getProperty('cell'));
*
* if (graph.isEnabled() && graph.isCellResizable(state.cell) && Math.abs(1 - evt.scale) > 0.2)
* {
* var scale = graph.view.scale;
* var tr = graph.view.translate;
*
* var w = state.width * evt.scale;
* var h = state.height * evt.scale;
* var x = state.x - (w - state.width) / 2;
* var y = state.y - (h - state.height) / 2;
*
* var bounds = new mxRectangle(graph.snap(x / scale) - tr.x,
* graph.snap(y / scale) - tr.y, graph.snap(w / scale), graph.snap(h / scale));
* graph.resizeCell(state.cell, bounds);
* eo.consume();
* }
* });
* (end)
*
* Parameters:
*
* evt - Gestureend event that represents the gesture.
* cell - Optional <mxCell> associated with the gesture.
*/
mxGraph.prototype.fireGestureEvent = function(evt, cell)
{
// Resets double tap event handling when gestures take place
this.lastTouchTime = 0;
this.fireEvent(new mxEventObject(mxEvent.GESTURE, 'event', evt, 'cell', cell));
};
/**
* Function: destroy
*
* Destroys the graph and all its resources.
*/
mxGraph.prototype.destroy = function()
{
if (!this.destroyed)
{
this.destroyed = true;
if (this.tooltipHandler != null)
{
this.tooltipHandler.destroy();
}
if (this.selectionCellsHandler != null)
{
this.selectionCellsHandler.destroy();
}
if (this.panningHandler != null)
{
this.panningHandler.destroy();
}
if (this.popupMenuHandler != null)
{
this.popupMenuHandler.destroy();
}
if (this.connectionHandler != null)
{
this.connectionHandler.destroy();
}
if (this.graphHandler != null)
{
this.graphHandler.destroy();
}
if (this.cellEditor != null)
{
this.cellEditor.destroy();
}
if (this.view != null)
{
this.view.destroy();
}
if (this.model != null && this.graphModelChangeListener != null)
{
this.model.removeListener(this.graphModelChangeListener);
this.graphModelChangeListener = null;
}
this.container = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellOverlay
*
* Extends <mxEventSource> to implement a graph overlay, represented by an icon
* and a tooltip. Overlays can handle and fire <click> events and are added to
* the graph using <mxGraph.addCellOverlay>, and removed using
* <mxGraph.removeCellOverlay>, or <mxGraph.removeCellOverlays> to remove all overlays.
* The <mxGraph.getCellOverlays> function returns the array of overlays for a given
* cell in a graph. If multiple overlays exist for the same cell, then
* <getBounds> should be overridden in at least one of the overlays.
*
* Overlays appear on top of all cells in a special layer. If this is not
* desirable, then the image must be rendered as part of the shape or label of
* the cell instead.
*
* Example:
*
* The following adds a new overlays for a given vertex and selects the cell
* if the overlay is clicked.
*
* (code)
* var overlay = new mxCellOverlay(img, html);
* graph.addCellOverlay(vertex, overlay);
* overlay.addListener(mxEvent.CLICK, function(sender, evt)
* {
* var cell = evt.getProperty('cell');
* graph.setSelectionCell(cell);
* });
* (end)
*
* For cell overlays to be printed use <mxPrintPreview.printOverlays>.
*
* Event: mxEvent.CLICK
*
* Fires when the user clicks on the overlay. The <code>event</code> property
* contains the corresponding mouse event and the <code>cell</code> property
* contains the cell. For touch devices this is fired if the element receives
* a touchend event.
*
* Constructor: mxCellOverlay
*
* Constructs a new overlay using the given image and tooltip.
*
* Parameters:
*
* image - <mxImage> that represents the icon to be displayed.
* tooltip - Optional string that specifies the tooltip.
* align - Optional horizontal alignment for the overlay. Possible
* values are <ALIGN_LEFT>, <ALIGN_CENTER> and <ALIGN_RIGHT>
* (default).
* verticalAlign - Vertical alignment for the overlay. Possible
* values are <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM>
* (default).
*/
function mxCellOverlay(image, tooltip, align, verticalAlign, offset, cursor)
{
this.image = image;
this.tooltip = tooltip;
this.align = (align != null) ? align : this.align;
this.verticalAlign = (verticalAlign != null) ? verticalAlign : this.verticalAlign;
this.offset = (offset != null) ? offset : new mxPoint();
this.cursor = (cursor != null) ? cursor : 'help';
};
/**
* Extends mxEventSource.
*/
mxCellOverlay.prototype = new mxEventSource();
mxCellOverlay.prototype.constructor = mxCellOverlay;
/**
* Variable: image
*
* Holds the <mxImage> to be used as the icon.
*/
mxCellOverlay.prototype.image = null;
/**
* Variable: tooltip
*
* Holds the optional string to be used as the tooltip.
*/
mxCellOverlay.prototype.tooltip = null;
/**
* Variable: align
*
* Holds the horizontal alignment for the overlay. Default is
* <mxConstants.ALIGN_RIGHT>. For edges, the overlay always appears in the
* center of the edge.
*/
mxCellOverlay.prototype.align = mxConstants.ALIGN_RIGHT;
/**
* Variable: verticalAlign
*
* Holds the vertical alignment for the overlay. Default is
* <mxConstants.ALIGN_BOTTOM>. For edges, the overlay always appears in the
* center of the edge.
*/
mxCellOverlay.prototype.verticalAlign = mxConstants.ALIGN_BOTTOM;
/**
* Variable: offset
*
* Holds the offset as an <mxPoint>. The offset will be scaled according to the
* current scale.
*/
mxCellOverlay.prototype.offset = null;
/**
* Variable: cursor
*
* Holds the cursor for the overlay. Default is 'help'.
*/
mxCellOverlay.prototype.cursor = null;
/**
* Variable: defaultOverlap
*
* Defines the overlapping for the overlay, that is, the proportional distance
* from the origin to the point defined by the alignment. Default is 0.5.
*/
mxCellOverlay.prototype.defaultOverlap = 0.5;
/**
* Function: getBounds
*
* Returns the bounds of the overlay for the given <mxCellState> as an
* <mxRectangle>. This should be overridden when using multiple overlays
* per cell so that the overlays do not overlap.
*
* The following example will place the overlay along an edge (where
* x=[-1..1] from the start to the end of the edge and y is the
* orthogonal offset in px).
*
* (code)
* overlay.getBounds = function(state)
* {
* var bounds = mxCellOverlay.prototype.getBounds.apply(this, arguments);
*
* if (state.view.graph.getModel().isEdge(state.cell))
* {
* var pt = state.view.getPoint(state, {x: 0, y: 0, relative: true});
*
* bounds.x = pt.x - bounds.width / 2;
* bounds.y = pt.y - bounds.height / 2;
* }
*
* return bounds;
* };
* (end)
*
* Parameters:
*
* state - <mxCellState> that represents the current state of the
* associated cell.
*/
mxCellOverlay.prototype.getBounds = function(state)
{
var isEdge = state.view.graph.getModel().isEdge(state.cell);
var s = state.view.scale;
var pt = null;
var w = this.image.width;
var h = this.image.height;
if (isEdge)
{
var pts = state.absolutePoints;
if (pts.length % 2 == 1)
{
pt = pts[Math.floor(pts.length / 2)];
}
else
{
var idx = pts.length / 2;
var p0 = pts[idx-1];
var p1 = pts[idx];
pt = new mxPoint(p0.x + (p1.x - p0.x) / 2,
p0.y + (p1.y - p0.y) / 2);
}
}
else
{
pt = new mxPoint();
if (this.align == mxConstants.ALIGN_LEFT)
{
pt.x = state.x;
}
else if (this.align == mxConstants.ALIGN_CENTER)
{
pt.x = state.x + state.width / 2;
}
else
{
pt.x = state.x + state.width;
}
if (this.verticalAlign == mxConstants.ALIGN_TOP)
{
pt.y = state.y;
}
else if (this.verticalAlign == mxConstants.ALIGN_MIDDLE)
{
pt.y = state.y + state.height / 2;
}
else
{
pt.y = state.y + state.height;
}
}
return new mxRectangle(Math.round(pt.x - (w * this.defaultOverlap - this.offset.x) * s),
Math.round(pt.y - (h * this.defaultOverlap - this.offset.y) * s), w * s, h * s);
};
/**
* Function: toString
*
* Returns the textual representation of the overlay to be used as the
* tooltip. This implementation returns <tooltip>.
*/
mxCellOverlay.prototype.toString = function()
{
return this.tooltip;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxOutline
*
* Implements an outline (aka overview) for a graph. Set <updateOnPan> to true
* to enable updates while the source graph is panning.
*
* Example:
*
* (code)
* var outline = new mxOutline(graph, div);
* (end)
*
* If an outline is used in an <mxWindow> in IE8 standards mode, the following
* code makes sure that the shadow filter is not inherited and that any
* transparent elements in the graph do not show the page background, but the
* background of the graph container.
*
* (code)
* if (document.documentMode == 8)
* {
* container.style.filter = 'progid:DXImageTransform.Microsoft.alpha(opacity=100)';
* }
* (end)
*
* To move the graph to the top, left corner the following code can be used.
*
* (code)
* var scale = graph.view.scale;
* var bounds = graph.getGraphBounds();
* graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale);
* (end)
*
* To toggle the suspended mode, the following can be used.
*
* (code)
* outline.suspended = !outln.suspended;
* if (!outline.suspended)
* {
* outline.update(true);
* }
* (end)
*
* Constructor: mxOutline
*
* Constructs a new outline for the specified graph inside the given
* container.
*
* Parameters:
*
* source - <mxGraph> to create the outline for.
* container - DOM node that will contain the outline.
*/
function mxOutline(source, container)
{
this.source = source;
if (container != null)
{
this.init(container);
}
};
/**
* Function: source
*
* Reference to the source <mxGraph>.
*/
mxOutline.prototype.source = null;
/**
* Function: outline
*
* Reference to the <mxGraph> that renders the outline.
*/
mxOutline.prototype.outline = null;
/**
* Function: graphRenderHint
*
* Renderhint to be used for the outline graph. Default is faster.
*/
mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_FASTER;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxOutline.prototype.enabled = true;
/**
* Variable: showViewport
*
* Specifies a viewport rectangle should be shown. Default is true.
*/
mxOutline.prototype.showViewport = true;
/**
* Variable: border
*
* Border to be added at the bottom and right. Default is 10.
*/
mxOutline.prototype.border = 10;
/**
* Variable: enabled
*
* Specifies the size of the sizer handler. Default is 8.
*/
mxOutline.prototype.sizerSize = 8;
/**
* Variable: labelsVisible
*
* Specifies if labels should be visible in the outline. Default is false.
*/
mxOutline.prototype.labelsVisible = false;
/**
* Variable: updateOnPan
*
* Specifies if <update> should be called for <mxEvent.PAN> in the source
* graph. Default is false.
*/
mxOutline.prototype.updateOnPan = false;
/**
* Variable: sizerImage
*
* Optional <mxImage> to be used for the sizer. Default is null.
*/
mxOutline.prototype.sizerImage = null;
/**
* Variable: minScale
*
* Minimum scale to be used. Default is 0.0001.
*/
mxOutline.prototype.minScale = 0.0001;
/**
* Variable: suspended
*
* Optional boolean flag to suspend updates. Default is false. This flag will
* also suspend repaints of the outline. To toggle this switch, use the
* following code.
*
* (code)
* nav.suspended = !nav.suspended;
*
* if (!nav.suspended)
* {
* nav.update(true);
* }
* (end)
*/
mxOutline.prototype.suspended = false;
/**
* Variable: forceVmlHandles
*
* Specifies if VML should be used to render the handles in this control. This
* is true for IE8 standards mode and false for all other browsers and modes.
* This is a workaround for rendering issues of HTML elements over elements
* with filters in IE 8 standards mode.
*/
mxOutline.prototype.forceVmlHandles = document.documentMode == 8;
/**
* Function: createGraph
*
* Creates the <mxGraph> used in the outline.
*/
mxOutline.prototype.createGraph = function(container)
{
var graph = new mxGraph(container, this.source.getModel(), this.graphRenderHint, this.source.getStylesheet());
graph.foldingEnabled = false;
graph.autoScroll = false;
return graph;
};
/**
* Function: init
*
* Initializes the outline inside the given container.
*/
mxOutline.prototype.init = function(container)
{
this.outline = this.createGraph(container);
// Do not repaint when suspended
var outlineGraphModelChanged = this.outline.graphModelChanged;
this.outline.graphModelChanged = mxUtils.bind(this, function(changes)
{
if (!this.suspended && this.outline != null)
{
outlineGraphModelChanged.apply(this.outline, arguments);
}
});
// Enables faster painting in SVG
if (mxClient.IS_SVG)
{
var node = this.outline.getView().getCanvas().parentNode;
node.setAttribute('shape-rendering', 'optimizeSpeed');
node.setAttribute('image-rendering', 'optimizeSpeed');
}
// Hides cursors and labels
this.outline.labelsVisible = this.labelsVisible;
this.outline.setEnabled(false);
this.updateHandler = mxUtils.bind(this, function(sender, evt)
{
if (!this.suspended && !this.active)
{
this.update();
}
});
// Updates the scale of the outline after a change of the main graph
this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler);
this.outline.addMouseListener(this);
// Adds listeners to keep the outline in sync with the source graph
var view = this.source.getView();
view.addListener(mxEvent.SCALE, this.updateHandler);
view.addListener(mxEvent.TRANSLATE, this.updateHandler);
view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler);
view.addListener(mxEvent.DOWN, this.updateHandler);
view.addListener(mxEvent.UP, this.updateHandler);
// Updates blue rectangle on scroll
mxEvent.addListener(this.source.container, 'scroll', this.updateHandler);
this.panHandler = mxUtils.bind(this, function(sender)
{
if (this.updateOnPan)
{
this.updateHandler.apply(this, arguments);
}
});
this.source.addListener(mxEvent.PAN, this.panHandler);
// Refreshes the graph in the outline after a refresh of the main graph
this.refreshHandler = mxUtils.bind(this, function(sender)
{
this.outline.setStylesheet(this.source.getStylesheet());
this.outline.refresh();
});
this.source.addListener(mxEvent.REFRESH, this.refreshHandler);
// Creates the blue rectangle for the viewport
this.bounds = new mxRectangle(0, 0, 0, 0);
this.selectionBorder = new mxRectangleShape(this.bounds, null,
mxConstants.OUTLINE_COLOR, mxConstants.OUTLINE_STROKEWIDTH);
this.selectionBorder.dialect = this.outline.dialect;
if (this.forceVmlHandles)
{
this.selectionBorder.isHtmlAllowed = function()
{
return false;
};
}
this.selectionBorder.init(this.outline.getView().getOverlayPane());
// Handles event by catching the initial pointer start and then listening to the
// complete gesture on the event target. This is needed because all the events
// are routed via the initial element even if that element is removed from the
// DOM, which happens when we repaint the selection border and zoom handles.
var handler = mxUtils.bind(this, function(evt)
{
var t = mxEvent.getSource(evt);
var redirect = mxUtils.bind(this, function(evt)
{
this.outline.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
});
var redirect2 = mxUtils.bind(this, function(evt)
{
mxEvent.removeGestureListeners(t, null, redirect, redirect2);
this.outline.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
});
mxEvent.addGestureListeners(t, null, redirect, redirect2);
this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
});
mxEvent.addGestureListeners(this.selectionBorder.node, handler);
// Creates a small blue rectangle for sizing (sizer handle)
this.sizer = this.createSizer();
if (this.forceVmlHandles)
{
this.sizer.isHtmlAllowed = function()
{
return false;
};
}
this.sizer.init(this.outline.getView().getOverlayPane());
if (this.enabled)
{
this.sizer.node.style.cursor = 'nwse-resize';
}
mxEvent.addGestureListeners(this.sizer.node, handler);
this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none';
this.sizer.node.style.display = this.selectionBorder.node.style.display;
this.selectionBorder.node.style.cursor = 'move';
this.update(false);
};
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxOutline.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* value - Boolean that specifies the new enabled state.
*/
mxOutline.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: setZoomEnabled
*
* Enables or disables the zoom handling by showing or hiding the respective
* handle.
*
* Parameters:
*
* value - Boolean that specifies the new enabled state.
*/
mxOutline.prototype.setZoomEnabled = function(value)
{
this.sizer.node.style.visibility = (value) ? 'visible' : 'hidden';
};
/**
* Function: refresh
*
* Invokes <update> and revalidate the outline. This method is deprecated.
*/
mxOutline.prototype.refresh = function()
{
this.update(true);
};
/**
* Function: createSizer
*
* Creates the shape used as the sizer.
*/
mxOutline.prototype.createSizer = function()
{
if (this.sizerImage != null)
{
var sizer = new mxImageShape(new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height), this.sizerImage.src);
sizer.dialect = this.outline.dialect;
return sizer;
}
else
{
var sizer = new mxRectangleShape(new mxRectangle(0, 0, this.sizerSize, this.sizerSize),
mxConstants.OUTLINE_HANDLE_FILLCOLOR, mxConstants.OUTLINE_HANDLE_STROKECOLOR);
sizer.dialect = this.outline.dialect;
return sizer;
}
};
/**
* Function: getSourceContainerSize
*
* Returns the size of the source container.
*/
mxOutline.prototype.getSourceContainerSize = function()
{
return new mxRectangle(0, 0, this.source.container.scrollWidth, this.source.container.scrollHeight);
};
/**
* Function: getOutlineOffset
*
* Returns the offset for drawing the outline graph.
*/
mxOutline.prototype.getOutlineOffset = function(scale)
{
return null;
};
/**
* Function: getOutlineOffset
*
* Returns the offset for drawing the outline graph.
*/
mxOutline.prototype.getSourceGraphBounds = function()
{
return this.source.getGraphBounds();
};
/**
* Function: update
*
* Updates the outline.
*/
mxOutline.prototype.update = function(revalidate)
{
if (this.source != null && this.source.container != null &&
this.outline != null && this.outline.container != null)
{
var sourceScale = this.source.view.scale;
var scaledGraphBounds = this.getSourceGraphBounds();
var unscaledGraphBounds = new mxRectangle(scaledGraphBounds.x / sourceScale + this.source.panDx,
scaledGraphBounds.y / sourceScale + this.source.panDy, scaledGraphBounds.width / sourceScale,
scaledGraphBounds.height / sourceScale);
var unscaledFinderBounds = new mxRectangle(0, 0,
this.source.container.clientWidth / sourceScale,
this.source.container.clientHeight / sourceScale);
var union = unscaledGraphBounds.clone();
union.add(unscaledFinderBounds);
// Zooms to the scrollable area if that is bigger than the graph
var size = this.getSourceContainerSize();
var completeWidth = Math.max(size.width / sourceScale, union.width);
var completeHeight = Math.max(size.height / sourceScale, union.height);
var availableWidth = Math.max(0, this.outline.container.clientWidth - this.border);
var availableHeight = Math.max(0, this.outline.container.clientHeight - this.border);
var outlineScale = Math.min(availableWidth / completeWidth, availableHeight / completeHeight);
var scale = (isNaN(outlineScale)) ? this.minScale : Math.max(this.minScale, outlineScale);
if (scale > 0)
{
if (this.outline.getView().scale != scale)
{
this.outline.getView().scale = scale;
revalidate = true;
}
var navView = this.outline.getView();
if (navView.currentRoot != this.source.getView().currentRoot)
{
navView.setCurrentRoot(this.source.getView().currentRoot);
}
var t = this.source.view.translate;
var tx = t.x + this.source.panDx;
var ty = t.y + this.source.panDy;
var off = this.getOutlineOffset(scale);
if (off != null)
{
tx += off.x;
ty += off.y;
}
if (unscaledGraphBounds.x < 0)
{
tx = tx - unscaledGraphBounds.x;
}
if (unscaledGraphBounds.y < 0)
{
ty = ty - unscaledGraphBounds.y;
}
if (navView.translate.x != tx || navView.translate.y != ty)
{
navView.translate.x = tx;
navView.translate.y = ty;
revalidate = true;
}
// Prepares local variables for computations
var t2 = navView.translate;
scale = this.source.getView().scale;
var scale2 = scale / navView.scale;
var scale3 = 1.0 / navView.scale;
var container = this.source.container;
// Updates the bounds of the viewrect in the navigation
this.bounds = new mxRectangle(
(t2.x - t.x - this.source.panDx) / scale3,
(t2.y - t.y - this.source.panDy) / scale3,
(container.clientWidth / scale2),
(container.clientHeight / scale2));
// Adds the scrollbar offset to the finder
this.bounds.x += this.source.container.scrollLeft * navView.scale / scale;
this.bounds.y += this.source.container.scrollTop * navView.scale / scale;
var b = this.selectionBorder.bounds;
if (b.x != this.bounds.x || b.y != this.bounds.y || b.width != this.bounds.width || b.height != this.bounds.height)
{
this.selectionBorder.bounds = this.bounds;
this.selectionBorder.redraw();
}
// Updates the bounds of the zoom handle at the bottom right
var b = this.sizer.bounds;
var b2 = new mxRectangle(this.bounds.x + this.bounds.width - b.width / 2,
this.bounds.y + this.bounds.height - b.height / 2, b.width, b.height);
if (b.x != b2.x || b.y != b2.y || b.width != b2.width || b.height != b2.height)
{
this.sizer.bounds = b2;
// Avoids update of visibility in redraw for VML
if (this.sizer.node.style.visibility != 'hidden')
{
this.sizer.redraw();
}
}
if (revalidate)
{
this.outline.view.revalidate();
}
}
}
};
/**
* Function: mouseDown
*
* Handles the event by starting a translation or zoom.
*/
mxOutline.prototype.mouseDown = function(sender, me)
{
if (this.enabled && this.showViewport)
{
var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.source.tolerance : 0;
var hit = (this.source.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ?
new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null;
this.zoom = me.isSource(this.sizer) || (hit != null && mxUtils.intersects(shape.bounds, hit));
this.startX = me.getX();
this.startY = me.getY();
this.active = true;
if (this.source.useScrollbarsForPanning && mxUtils.hasScrollbars(this.source.container))
{
this.dx0 = this.source.container.scrollLeft;
this.dy0 = this.source.container.scrollTop;
}
else
{
this.dx0 = 0;
this.dy0 = 0;
}
}
me.consume();
};
/**
* Function: mouseMove
*
* Handles the event by previewing the viewrect in <graph> and updating the
* rectangle that represents the viewrect in the outline.
*/
mxOutline.prototype.mouseMove = function(sender, me)
{
if (this.active)
{
this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none';
this.sizer.node.style.display = this.selectionBorder.node.style.display;
var delta = this.getTranslateForEvent(me);
var dx = delta.x;
var dy = delta.y;
var bounds = null;
if (!this.zoom)
{
// Previews the panning on the source graph
var scale = this.outline.getView().scale;
bounds = new mxRectangle(this.bounds.x + dx,
this.bounds.y + dy, this.bounds.width, this.bounds.height);
this.selectionBorder.bounds = bounds;
this.selectionBorder.redraw();
dx /= scale;
dx *= this.source.getView().scale;
dy /= scale;
dy *= this.source.getView().scale;
this.source.panGraph(-dx - this.dx0, -dy - this.dy0);
}
else
{
// Does *not* preview zooming on the source graph
var container = this.source.container;
var viewRatio = container.clientWidth / container.clientHeight;
dy = dx / viewRatio;
bounds = new mxRectangle(this.bounds.x,
this.bounds.y,
Math.max(1, this.bounds.width + dx),
Math.max(1, this.bounds.height + dy));
this.selectionBorder.bounds = bounds;
this.selectionBorder.redraw();
}
// Updates the zoom handle
var b = this.sizer.bounds;
this.sizer.bounds = new mxRectangle(
bounds.x + bounds.width - b.width / 2,
bounds.y + bounds.height - b.height / 2,
b.width, b.height);
// Avoids update of visibility in redraw for VML
if (this.sizer.node.style.visibility != 'hidden')
{
this.sizer.redraw();
}
me.consume();
}
};
/**
* Function: getTranslateForEvent
*
* Gets the translate for the given mouse event. Here is an example to limit
* the outline to stay within positive coordinates:
*
* (code)
* outline.getTranslateForEvent = function(me)
* {
* var pt = new mxPoint(me.getX() - this.startX, me.getY() - this.startY);
*
* if (!this.zoom)
* {
* var tr = this.source.view.translate;
* pt.x = Math.max(tr.x * this.outline.view.scale, pt.x);
* pt.y = Math.max(tr.y * this.outline.view.scale, pt.y);
* }
*
* return pt;
* };
* (end)
*/
mxOutline.prototype.getTranslateForEvent = function(me)
{
return new mxPoint(me.getX() - this.startX, me.getY() - this.startY);
};
/**
* Function: mouseUp
*
* Handles the event by applying the translation or zoom to <graph>.
*/
mxOutline.prototype.mouseUp = function(sender, me)
{
if (this.active)
{
var delta = this.getTranslateForEvent(me);
var dx = delta.x;
var dy = delta.y;
if (Math.abs(dx) > 0 || Math.abs(dy) > 0)
{
if (!this.zoom)
{
// Applies the new translation if the source
// has no scrollbars
if (!this.source.useScrollbarsForPanning ||
!mxUtils.hasScrollbars(this.source.container))
{
this.source.panGraph(0, 0);
dx /= this.outline.getView().scale;
dy /= this.outline.getView().scale;
var t = this.source.getView().translate;
this.source.getView().setTranslate(t.x - dx, t.y - dy);
}
}
else
{
// Applies the new zoom
var w = this.selectionBorder.bounds.width;
var scale = this.source.getView().scale;
this.source.zoomTo(Math.max(this.minScale, scale - (dx * scale) / w), false);
}
this.update();
me.consume();
}
// Resets the state of the handler
this.index = null;
this.active = false;
}
};
/**
* Function: destroy
*
* Destroy this outline and removes all listeners from <source>.
*/
mxOutline.prototype.destroy = function()
{
if (this.source != null)
{
this.source.removeListener(this.panHandler);
this.source.removeListener(this.refreshHandler);
this.source.getModel().removeListener(this.updateHandler);
this.source.getView().removeListener(this.updateHandler);
mxEvent.removeListener(this.source.container, 'scroll', this.updateHandler);
this.source = null;
}
if (this.outline != null)
{
this.outline.removeMouseListener(this);
this.outline.destroy();
this.outline = null;
}
if (this.selectionBorder != null)
{
this.selectionBorder.destroy();
this.selectionBorder = null;
}
if (this.sizer != null)
{
this.sizer.destroy();
this.sizer = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxMultiplicity
*
* Defines invalid connections along with the error messages that they produce.
* To add or remove rules on a graph, you must add/remove instances of this
* class to <mxGraph.multiplicities>.
*
* Example:
*
* (code)
* graph.multiplicities.push(new mxMultiplicity(
* true, 'rectangle', null, null, 0, 2, ['circle'],
* 'Only 2 targets allowed',
* 'Only circle targets allowed'));
* (end)
*
* Defines a rule where each rectangle must be connected to no more than 2
* circles and no other types of targets are allowed.
*
* Constructor: mxMultiplicity
*
* Instantiate class mxMultiplicity in order to describe allowed
* connections in a graph. Not all constraints can be enforced while
* editing, some must be checked at validation time. The <countError> and
* <typeError> are treated as resource keys in <mxResources>.
*
* Parameters:
*
* source - Boolean indicating if this rule applies to the source or target
* terminal.
* type - Type of the source or target terminal that this rule applies to.
* See <type> for more information.
* attr - Optional attribute name to match the source or target terminal.
* value - Optional attribute value to match the source or target terminal.
* min - Minimum number of edges for this rule. Default is 1.
* max - Maximum number of edges for this rule. n means infinite. Default
* is n.
* validNeighbors - Array of types of the opposite terminal for which this
* rule applies.
* countError - Error to be displayed for invalid number of edges.
* typeError - Error to be displayed for invalid opposite terminals.
* validNeighborsAllowed - Optional boolean indicating if the array of
* opposite types should be valid or invalid.
*/
function mxMultiplicity(source, type, attr, value, min, max,
validNeighbors, countError, typeError, validNeighborsAllowed)
{
this.source = source;
this.type = type;
this.attr = attr;
this.value = value;
this.min = (min != null) ? min : 0;
this.max = (max != null) ? max : 'n';
this.validNeighbors = validNeighbors;
this.countError = mxResources.get(countError) || countError;
this.typeError = mxResources.get(typeError) || typeError;
this.validNeighborsAllowed = (validNeighborsAllowed != null) ?
validNeighborsAllowed : true;
};
/**
* Variable: type
*
* Defines the type of the source or target terminal. The type is a string
* passed to <mxUtils.isNode> together with the source or target vertex
* value as the first argument.
*/
mxMultiplicity.prototype.type = null;
/**
* Variable: attr
*
* Optional string that specifies the attributename to be passed to
* <mxUtils.isNode> to check if the rule applies to a cell.
*/
mxMultiplicity.prototype.attr = null;
/**
* Variable: value
*
* Optional string that specifies the value of the attribute to be passed
* to <mxUtils.isNode> to check if the rule applies to a cell.
*/
mxMultiplicity.prototype.value = null;
/**
* Variable: source
*
* Boolean that specifies if the rule is applied to the source or target
* terminal of an edge.
*/
mxMultiplicity.prototype.source = null;
/**
* Variable: min
*
* Defines the minimum number of connections for which this rule applies.
* Default is 0.
*/
mxMultiplicity.prototype.min = null;
/**
* Variable: max
*
* Defines the maximum number of connections for which this rule applies.
* A value of 'n' means unlimited times. Default is 'n'.
*/
mxMultiplicity.prototype.max = null;
/**
* Variable: validNeighbors
*
* Holds an array of strings that specify the type of neighbor for which
* this rule applies. The strings are used in <mxCell.is> on the opposite
* terminal to check if the rule applies to the connection.
*/
mxMultiplicity.prototype.validNeighbors = null;
/**
* Variable: validNeighborsAllowed
*
* Boolean indicating if the list of validNeighbors are those that are allowed
* for this rule or those that are not allowed for this rule.
*/
mxMultiplicity.prototype.validNeighborsAllowed = true;
/**
* Variable: countError
*
* Holds the localized error message to be displayed if the number of
* connections for which the rule applies is smaller than <min> or greater
* than <max>.
*/
mxMultiplicity.prototype.countError = null;
/**
* Variable: typeError
*
* Holds the localized error message to be displayed if the type of the
* neighbor for a connection does not match the rule.
*/
mxMultiplicity.prototype.typeError = null;
/**
* Function: check
*
* Checks the multiplicity for the given arguments and returns the error
* for the given connection or null if the multiplicity does not apply.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph> instance.
* edge - <mxCell> that represents the edge to validate.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
* sourceOut - Number of outgoing edges from the source terminal.
* targetIn - Number of incoming edges for the target terminal.
*/
mxMultiplicity.prototype.check = function(graph, edge, source, target, sourceOut, targetIn)
{
var error = '';
if ((this.source && this.checkTerminal(graph, source, edge)) ||
(!this.source && this.checkTerminal(graph, target, edge)))
{
if (this.countError != null &&
((this.source && (this.max == 0 || (sourceOut >= this.max))) ||
(!this.source && (this.max == 0 || (targetIn >= this.max)))))
{
error += this.countError + '\n';
}
if (this.validNeighbors != null && this.typeError != null && this.validNeighbors.length > 0)
{
var isValid = this.checkNeighbors(graph, edge, source, target);
if (!isValid)
{
error += this.typeError + '\n';
}
}
}
return (error.length > 0) ? error : null;
};
/**
* Function: checkNeighbors
*
* Checks if there are any valid neighbours in <validNeighbors>. This is only
* called if <validNeighbors> is a non-empty array.
*/
mxMultiplicity.prototype.checkNeighbors = function(graph, edge, source, target)
{
var sourceValue = graph.model.getValue(source);
var targetValue = graph.model.getValue(target);
var isValid = !this.validNeighborsAllowed;
var valid = this.validNeighbors;
for (var j = 0; j < valid.length; j++)
{
if (this.source &&
this.checkType(graph, targetValue, valid[j]))
{
isValid = this.validNeighborsAllowed;
break;
}
else if (!this.source &&
this.checkType(graph, sourceValue, valid[j]))
{
isValid = this.validNeighborsAllowed;
break;
}
}
return isValid;
};
/**
* Function: checkTerminal
*
* Checks the given terminal cell and returns true if this rule applies. The
* given cell is the source or target of the given edge, depending on
* <source>. This implementation uses <checkType> on the terminal's value.
*/
mxMultiplicity.prototype.checkTerminal = function(graph, terminal, edge)
{
var value = graph.model.getValue(terminal);
return this.checkType(graph, value, this.type, this.attr, this.value);
};
/**
* Function: checkType
*
* Checks the type of the given value.
*/
mxMultiplicity.prototype.checkType = function(graph, value, type, attr, attrValue)
{
if (value != null)
{
if (!isNaN(value.nodeType)) // Checks if value is a DOM node
{
return mxUtils.isNode(value, type, attr, attrValue);
}
else
{
return value == type;
}
}
return false;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxLayoutManager
*
* Implements a layout manager that runs a given layout after any changes to the graph:
*
* Example:
*
* (code)
* var layoutMgr = new mxLayoutManager(graph);
* layoutMgr.getLayout = function(cell)
* {
* return layout;
* };
* (end)
*
* Event: mxEvent.LAYOUT_CELLS
*
* Fires between begin- and endUpdate after all cells have been layouted in
* <layoutCells>. The <code>cells</code> property contains all cells that have
* been passed to <layoutCells>.
*
* Constructor: mxLayoutManager
*
* Constructs a new automatic layout for the given graph.
*
* Arguments:
*
* graph - Reference to the enclosing graph.
*/
function mxLayoutManager(graph)
{
// Executes the layout before the changes are dispatched
this.undoHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.beforeUndo(evt.getProperty('edit'));
}
});
// Notifies the layout of a move operation inside a parent
this.moveHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.cellsMoved(evt.getProperty('cells'), evt.getProperty('event'));
}
});
this.setGraph(graph);
};
/**
* Extends mxEventSource.
*/
mxLayoutManager.prototype = new mxEventSource();
mxLayoutManager.prototype.constructor = mxLayoutManager;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxLayoutManager.prototype.graph = null;
/**
* Variable: bubbling
*
* Specifies if the layout should bubble along
* the cell hierarchy. Default is true.
*/
mxLayoutManager.prototype.bubbling = true;
/**
* Variable: enabled
*
* Specifies if event handling is enabled. Default is true.
*/
mxLayoutManager.prototype.enabled = true;
/**
* Variable: updateHandler
*
* Holds the function that handles the endUpdate event.
*/
mxLayoutManager.prototype.updateHandler = null;
/**
* Variable: moveHandler
*
* Holds the function that handles the move event.
*/
mxLayoutManager.prototype.moveHandler = null;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxLayoutManager.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxLayoutManager.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isBubbling
*
* Returns true if a layout should bubble, that is, if the parent layout
* should be executed whenever a cell layout (layout of the children of
* a cell) has been executed. This implementation returns <bubbling>.
*/
mxLayoutManager.prototype.isBubbling = function()
{
return this.bubbling;
};
/**
* Function: setBubbling
*
* Sets <bubbling>.
*/
mxLayoutManager.prototype.setBubbling = function(value)
{
this.bubbling = value;
};
/**
* Function: getGraph
*
* Returns the graph that this layout operates on.
*/
mxLayoutManager.prototype.getGraph = function()
{
return this.graph;
};
/**
* Function: setGraph
*
* Sets the graph that the layouts operate on.
*/
mxLayoutManager.prototype.setGraph = function(graph)
{
if (this.graph != null)
{
var model = this.graph.getModel();
model.removeListener(this.undoHandler);
this.graph.removeListener(this.moveHandler);
}
this.graph = graph;
if (this.graph != null)
{
var model = this.graph.getModel();
model.addListener(mxEvent.BEFORE_UNDO, this.undoHandler);
this.graph.addListener(mxEvent.MOVE_CELLS, this.moveHandler);
}
};
/**
* Function: getLayout
*
* Returns the layout to be executed for the given graph and parent.
*/
mxLayoutManager.prototype.getLayout = function(parent)
{
return null;
};
/**
* Function: beforeUndo
*
* Called from the undoHandler.
*
* Parameters:
*
* cell - Array of <mxCells> that have been moved.
* evt - Mouse event that represents the mousedown.
*/
mxLayoutManager.prototype.beforeUndo = function(undoableEdit)
{
var cells = this.getCellsForChanges(undoableEdit.changes);
var model = this.getGraph().getModel();
// Adds all descendants
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
tmp = tmp.concat(model.getDescendants(cells[i]));
}
cells = tmp;
// Adds all parent ancestors
if (this.isBubbling())
{
tmp = model.getParents(cells);
while (tmp.length > 0)
{
cells = cells.concat(tmp);
tmp = model.getParents(tmp);
}
}
this.executeLayoutForCells(cells);
};
/**
* Function: executeLayout
*
* Executes the given layout on the given parent.
*/
mxLayoutManager.prototype.executeLayoutForCells = function(cells)
{
// Adds reverse to this array to avoid duplicate execution of leafes
// Works like capture/bubble for events, first executes all layout
// from top to bottom and in reverse order and removes duplicates.
var sorted = mxUtils.sortCells(cells, true);
sorted = sorted.concat(sorted.slice().reverse());
this.layoutCells(sorted);
};
/**
* Function: cellsMoved
*
* Called from the moveHandler.
*
* Parameters:
*
* cell - Array of <mxCells> that have been moved.
* evt - Mouse event that represents the mousedown.
*/
mxLayoutManager.prototype.cellsMoved = function(cells, evt)
{
if (cells != null && evt != null)
{
var point = mxUtils.convertPoint(this.getGraph().container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
var model = this.getGraph().getModel();
// Checks if a layout exists to take care of the moving if the
// parent itself is not being moved
for (var i = 0; i < cells.length; i++)
{
var parent = model.getParent(cells[i]);
if (mxUtils.indexOf(cells, parent) < 0)
{
var layout = this.getLayout(parent);
if (layout != null)
{
layout.moveCell(cells[i], point.x, point.y);
}
}
}
}
};
/**
* Function: getCellsForEdit
*
* Returns the cells to be layouted for the given sequence of changes.
*/
mxLayoutManager.prototype.getCellsForChanges = function(changes)
{
var dict = new mxDictionary();
var result = [];
for (var i = 0; i < changes.length; i++)
{
var change = changes[i];
if (change instanceof mxRootChange)
{
return [];
}
else
{
var cells = this.getCellsForChange(change);
for (var j = 0; j < cells.length; j++)
{
if (cells[j] != null && !dict.get(cells[j]))
{
dict.put(cells[j], true);
result.push(cells[j]);
}
}
}
}
return result;
};
/**
* Function: getCellsForChange
*
* Executes all layouts which have been scheduled during the
* changes.
*/
mxLayoutManager.prototype.getCellsForChange = function(change)
{
var model = this.getGraph().getModel();
if (change instanceof mxChildChange)
{
return [change.child, change.previous, model.getParent(change.child)];
}
else if (change instanceof mxTerminalChange || change instanceof mxGeometryChange)
{
return [change.cell, model.getParent(change.cell)];
}
else if (change instanceof mxVisibleChange || change instanceof mxStyleChange)
{
return [change.cell];
}
return [];
};
/**
* Function: layoutCells
*
* Executes all layouts which have been scheduled during the
* changes.
*/
mxLayoutManager.prototype.layoutCells = function(cells)
{
if (cells.length > 0)
{
// Invokes the layouts while removing duplicates
var model = this.getGraph().getModel();
model.beginUpdate();
try
{
var last = null;
for (var i = 0; i < cells.length; i++)
{
if (cells[i] != model.getRoot() && cells[i] != last)
{
if (this.executeLayout(this.getLayout(cells[i]), cells[i]))
{
last = cells[i];
}
}
}
this.fireEvent(new mxEventObject(mxEvent.LAYOUT_CELLS, 'cells', cells));
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: executeLayout
*
* Executes the given layout on the given parent.
*/
mxLayoutManager.prototype.executeLayout = function(layout, parent)
{
var result = false;
if (layout != null && parent != null)
{
layout.execute(parent);
result = true;
}
return result;
};
/**
* Function: destroy
*
* Removes all handlers from the <graph> and deletes the reference to it.
*/
mxLayoutManager.prototype.destroy = function()
{
this.setGraph(null);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSwimlaneManager
*
* Manager for swimlanes and nested swimlanes that sets the size of newly added
* swimlanes to that of their siblings, and propagates changes to the size of a
* swimlane to its siblings, if <siblings> is true, and its ancestors, if
* <bubbling> is true.
*
* Constructor: mxSwimlaneManager
*
* Constructs a new swimlane manager for the given graph.
*
* Arguments:
*
* graph - Reference to the enclosing graph.
*/
function mxSwimlaneManager(graph, horizontal, addEnabled, resizeEnabled)
{
this.horizontal = (horizontal != null) ? horizontal : true;
this.addEnabled = (addEnabled != null) ? addEnabled : true;
this.resizeEnabled = (resizeEnabled != null) ? resizeEnabled : true;
this.addHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled() && this.isAddEnabled())
{
this.cellsAdded(evt.getProperty('cells'));
}
});
this.resizeHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled() && this.isResizeEnabled())
{
this.cellsResized(evt.getProperty('cells'));
}
});
this.setGraph(graph);
};
/**
* Extends mxEventSource.
*/
mxSwimlaneManager.prototype = new mxEventSource();
mxSwimlaneManager.prototype.constructor = mxSwimlaneManager;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxSwimlaneManager.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if event handling is enabled. Default is true.
*/
mxSwimlaneManager.prototype.enabled = true;
/**
* Variable: horizontal
*
* Specifies the orientation of the swimlanes. Default is true.
*/
mxSwimlaneManager.prototype.horizontal = true;
/**
* Variable: addEnabled
*
* Specifies if newly added cells should be resized to match the size of their
* existing siblings. Default is true.
*/
mxSwimlaneManager.prototype.addEnabled = true;
/**
* Variable: resizeEnabled
*
* Specifies if resizing of swimlanes should be handled. Default is true.
*/
mxSwimlaneManager.prototype.resizeEnabled = true;
/**
* Variable: moveHandler
*
* Holds the function that handles the move event.
*/
mxSwimlaneManager.prototype.addHandler = null;
/**
* Variable: moveHandler
*
* Holds the function that handles the move event.
*/
mxSwimlaneManager.prototype.resizeHandler = null;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxSwimlaneManager.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxSwimlaneManager.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: isHorizontal
*
* Returns <horizontal>.
*/
mxSwimlaneManager.prototype.isHorizontal = function()
{
return this.horizontal;
};
/**
* Function: setHorizontal
*
* Sets <horizontal>.
*/
mxSwimlaneManager.prototype.setHorizontal = function(value)
{
this.horizontal = value;
};
/**
* Function: isAddEnabled
*
* Returns <addEnabled>.
*/
mxSwimlaneManager.prototype.isAddEnabled = function()
{
return this.addEnabled;
};
/**
* Function: setAddEnabled
*
* Sets <addEnabled>.
*/
mxSwimlaneManager.prototype.setAddEnabled = function(value)
{
this.addEnabled = value;
};
/**
* Function: isResizeEnabled
*
* Returns <resizeEnabled>.
*/
mxSwimlaneManager.prototype.isResizeEnabled = function()
{
return this.resizeEnabled;
};
/**
* Function: setResizeEnabled
*
* Sets <resizeEnabled>.
*/
mxSwimlaneManager.prototype.setResizeEnabled = function(value)
{
this.resizeEnabled = value;
};
/**
* Function: getGraph
*
* Returns the graph that this manager operates on.
*/
mxSwimlaneManager.prototype.getGraph = function()
{
return this.graph;
};
/**
* Function: setGraph
*
* Sets the graph that the manager operates on.
*/
mxSwimlaneManager.prototype.setGraph = function(graph)
{
if (this.graph != null)
{
this.graph.removeListener(this.addHandler);
this.graph.removeListener(this.resizeHandler);
}
this.graph = graph;
if (this.graph != null)
{
this.graph.addListener(mxEvent.ADD_CELLS, this.addHandler);
this.graph.addListener(mxEvent.CELLS_RESIZED, this.resizeHandler);
}
};
/**
* Function: isSwimlaneIgnored
*
* Returns true if the given swimlane should be ignored.
*/
mxSwimlaneManager.prototype.isSwimlaneIgnored = function(swimlane)
{
return !this.getGraph().isSwimlane(swimlane);
};
/**
* Function: isCellHorizontal
*
* Returns true if the given cell is horizontal. If the given cell is not a
* swimlane, then the global orientation is returned.
*/
mxSwimlaneManager.prototype.isCellHorizontal = function(cell)
{
if (this.graph.isSwimlane(cell))
{
var style = this.graph.getCellStyle(cell);
return mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, 1) == 1;
}
return !this.isHorizontal();
};
/**
* Function: cellsAdded
*
* Called if any cells have been added.
*
* Parameters:
*
* cell - Array of <mxCells> that have been added.
*/
mxSwimlaneManager.prototype.cellsAdded = function(cells)
{
if (cells != null)
{
var model = this.getGraph().getModel();
model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
if (!this.isSwimlaneIgnored(cells[i]))
{
this.swimlaneAdded(cells[i]);
}
}
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: swimlaneAdded
*
* Updates the size of the given swimlane to match that of any existing
* siblings swimlanes.
*
* Parameters:
*
* swimlane - <mxCell> that represents the new swimlane.
*/
mxSwimlaneManager.prototype.swimlaneAdded = function(swimlane)
{
var model = this.getGraph().getModel();
var parent = model.getParent(swimlane);
var childCount = model.getChildCount(parent);
var geo = null;
// Finds the first valid sibling swimlane as reference
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(parent, i);
if (child != swimlane && !this.isSwimlaneIgnored(child))
{
geo = model.getGeometry(child);
if (geo != null)
{
break;
}
}
}
// Applies the size of the refernece to the newly added swimlane
if (geo != null)
{
var parentHorizontal = (parent != null) ? this.isCellHorizontal(parent) : this.horizontal;
this.resizeSwimlane(swimlane, geo.width, geo.height, parentHorizontal);
}
};
/**
* Function: cellsResized
*
* Called if any cells have been resizes. Calls <swimlaneResized> for all
* swimlanes where <isSwimlaneIgnored> returns false.
*
* Parameters:
*
* cells - Array of <mxCells> whose size was changed.
*/
mxSwimlaneManager.prototype.cellsResized = function(cells)
{
if (cells != null)
{
var model = this.getGraph().getModel();
model.beginUpdate();
try
{
// Finds the top-level swimlanes and adds offsets
for (var i = 0; i < cells.length; i++)
{
if (!this.isSwimlaneIgnored(cells[i]))
{
var geo = model.getGeometry(cells[i]);
if (geo != null)
{
var size = new mxRectangle(0, 0, geo.width, geo.height);
var top = cells[i];
var current = top;
while (current != null)
{
top = current;
current = model.getParent(current);
var tmp = (this.graph.isSwimlane(current)) ?
this.graph.getStartSize(current) :
new mxRectangle();
size.width += tmp.width;
size.height += tmp.height;
}
var parentHorizontal = (current != null) ? this.isCellHorizontal(current) : this.horizontal;
this.resizeSwimlane(top, size.width, size.height, parentHorizontal);
}
}
}
}
finally
{
model.endUpdate();
}
}
};
/**
* Function: resizeSwimlane
*
* Called from <cellsResized> for all swimlanes that are not ignored to update
* the size of the siblings and the size of the parent swimlanes, recursively,
* if <bubbling> is true.
*
* Parameters:
*
* swimlane - <mxCell> whose size has changed.
*/
mxSwimlaneManager.prototype.resizeSwimlane = function(swimlane, w, h, parentHorizontal)
{
var model = this.getGraph().getModel();
model.beginUpdate();
try
{
var horizontal = this.isCellHorizontal(swimlane);
if (!this.isSwimlaneIgnored(swimlane))
{
var geo = model.getGeometry(swimlane);
if (geo != null)
{
if ((parentHorizontal && geo.height != h) || (!parentHorizontal && geo.width != w))
{
geo = geo.clone();
if (parentHorizontal)
{
geo.height = h;
}
else
{
geo.width = w;
}
model.setGeometry(swimlane, geo);
}
}
}
var tmp = (this.graph.isSwimlane(swimlane)) ?
this.graph.getStartSize(swimlane) :
new mxRectangle();
w -= tmp.width;
h -= tmp.height;
var childCount = model.getChildCount(swimlane);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(swimlane, i);
this.resizeSwimlane(child, w, h, horizontal);
}
}
finally
{
model.endUpdate();
}
};
/**
* Function: destroy
*
* Removes all handlers from the <graph> and deletes the reference to it.
*/
mxSwimlaneManager.prototype.destroy = function()
{
this.setGraph(null);
};
/**
* Copyright (c) 2006-2017, JGraph Ltd
* Copyright (c) 2006-2017, Gaudenz Alder
*/
/**
* Class: mxTemporaryCellStates
*
* Creates a temporary set of cell states.
*/
function mxTemporaryCellStates(view, scale, cells, isCellVisibleFn, getLinkForCellState)
{
scale = (scale != null) ? scale : 1;
this.view = view;
// Stores the previous state
this.oldValidateCellState = view.validateCellState;
this.oldBounds = view.getGraphBounds();
this.oldStates = view.getStates();
this.oldScale = view.getScale();
this.oldDoRedrawShape = view.graph.cellRenderer.doRedrawShape;
var self = this;
// Overrides doRedrawShape and paint shape to add links on shapes
if (getLinkForCellState != null)
{
view.graph.cellRenderer.doRedrawShape = function(state)
{
var oldPaint = state.shape.paint;
state.shape.paint = function(c)
{
var link = getLinkForCellState(state);
if (link != null)
{
c.setLink(link);
}
oldPaint.apply(this, arguments);
if (link != null)
{
c.setLink(null);
}
};
self.oldDoRedrawShape.apply(view.graph.cellRenderer, arguments);
state.shape.paint = oldPaint;
};
}
// Overrides validateCellState to ignore invisible cells
view.validateCellState = function(cell, resurse)
{
if (cell == null || isCellVisibleFn == null || isCellVisibleFn(cell))
{
return self.oldValidateCellState.apply(view, arguments);
}
return null;
};
// Creates space for new states
view.setStates(new mxDictionary());
view.setScale(scale);
if (cells != null)
{
view.resetValidationState();
var bbox = null;
// Validates the vertices and edges without adding them to
// the model so that the original cells are not modified
for (var i = 0; i < cells.length; i++)
{
var bounds = view.getBoundingBox(view.validateCellState(view.validateCell(cells[i])));
if (bbox == null)
{
bbox = bounds;
}
else
{
bbox.add(bounds);
}
}
view.setGraphBounds(bbox || new mxRectangle());
}
};
/**
* Variable: view
*
* Holds the width of the rectangle. Default is 0.
*/
mxTemporaryCellStates.prototype.view = null;
/**
* Variable: oldStates
*
* Holds the height of the rectangle. Default is 0.
*/
mxTemporaryCellStates.prototype.oldStates = null;
/**
* Variable: oldBounds
*
* Holds the height of the rectangle. Default is 0.
*/
mxTemporaryCellStates.prototype.oldBounds = null;
/**
* Variable: oldScale
*
* Holds the height of the rectangle. Default is 0.
*/
mxTemporaryCellStates.prototype.oldScale = null;
/**
* Function: destroy
*
* Returns the top, left corner as a new <mxPoint>.
*/
mxTemporaryCellStates.prototype.destroy = function()
{
this.view.setScale(this.oldScale);
this.view.setStates(this.oldStates);
this.view.setGraphBounds(this.oldBounds);
this.view.validateCellState = this.oldValidateCellState;
this.view.graph.cellRenderer.doRedrawShape = this.oldDoRedrawShape;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
*
* Class: mxCellStatePreview
*
* Implements a live preview for moving cells.
*
* Constructor: mxCellStatePreview
*
* Constructs a move preview for the given graph.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxCellStatePreview(graph)
{
this.deltas = new mxDictionary();
this.graph = graph;
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellStatePreview.prototype.graph = null;
/**
* Variable: deltas
*
* Reference to the enclosing <mxGraph>.
*/
mxCellStatePreview.prototype.deltas = null;
/**
* Variable: count
*
* Contains the number of entries in the map.
*/
mxCellStatePreview.prototype.count = 0;
/**
* Function: isEmpty
*
* Returns true if this contains no entries.
*/
mxCellStatePreview.prototype.isEmpty = function()
{
return this.count == 0;
};
/**
* Function: moveState
*/
mxCellStatePreview.prototype.moveState = function(state, dx, dy, add, includeEdges)
{
add = (add != null) ? add : true;
includeEdges = (includeEdges != null) ? includeEdges : true;
var delta = this.deltas.get(state.cell);
if (delta == null)
{
// Note: Deltas stores the point and the state since the key is a string.
delta = {point: new mxPoint(dx, dy), state: state};
this.deltas.put(state.cell, delta);
this.count++;
}
else if (add)
{
delta.point.x += dx;
delta.point.y += dy;
}
else
{
delta.point.x = dx;
delta.point.y = dy;
}
if (includeEdges)
{
this.addEdges(state);
}
return delta.point;
};
/**
* Function: show
*/
mxCellStatePreview.prototype.show = function(visitor)
{
this.deltas.visit(mxUtils.bind(this, function(key, delta)
{
this.translateState(delta.state, delta.point.x, delta.point.y);
}));
this.deltas.visit(mxUtils.bind(this, function(key, delta)
{
this.revalidateState(delta.state, delta.point.x, delta.point.y, visitor);
}));
};
/**
* Function: translateState
*/
mxCellStatePreview.prototype.translateState = function(state, dx, dy)
{
if (state != null)
{
var model = this.graph.getModel();
if (model.isVertex(state.cell))
{
state.view.updateCellState(state);
var geo = model.getGeometry(state.cell);
// Moves selection cells and non-relative vertices in
// the first phase so that edge terminal points will
// be updated in the second phase
if ((dx != 0 || dy != 0) && geo != null && (!geo.relative || this.deltas.get(state.cell) != null))
{
state.x += dx;
state.y += dy;
}
}
var childCount = model.getChildCount(state.cell);
for (var i = 0; i < childCount; i++)
{
this.translateState(state.view.getState(model.getChildAt(state.cell, i)), dx, dy);
}
}
};
/**
* Function: revalidateState
*/
mxCellStatePreview.prototype.revalidateState = function(state, dx, dy, visitor)
{
if (state != null)
{
var model = this.graph.getModel();
// Updates the edge terminal points and restores the
// (relative) positions of any (relative) children
if (model.isEdge(state.cell))
{
state.view.updateCellState(state);
}
var geo = this.graph.getCellGeometry(state.cell);
var pState = state.view.getState(model.getParent(state.cell));
// Moves selection vertices which are relative
if ((dx != 0 || dy != 0) && geo != null && geo.relative &&
model.isVertex(state.cell) && (pState == null ||
model.isVertex(pState.cell) || this.deltas.get(state.cell) != null))
{
state.x += dx;
state.y += dy;
}
this.graph.cellRenderer.redraw(state);
// Invokes the visitor on the given state
if (visitor != null)
{
visitor(state);
}
var childCount = model.getChildCount(state.cell);
for (var i = 0; i < childCount; i++)
{
this.revalidateState(this.graph.view.getState(model.getChildAt(state.cell, i)), dx, dy, visitor);
}
}
};
/**
* Function: addEdges
*/
mxCellStatePreview.prototype.addEdges = function(state)
{
var model = this.graph.getModel();
var edgeCount = model.getEdgeCount(state.cell);
for (var i = 0; i < edgeCount; i++)
{
var s = state.view.getState(model.getEdgeAt(state.cell, i));
if (s != null)
{
this.moveState(s, 0, 0);
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConnectionConstraint
*
* Defines an object that contains the constraints about how to connect one
* side of an edge to its terminal.
*
* Constructor: mxConnectionConstraint
*
* Constructs a new connection constraint for the given point and boolean
* arguments.
*
* Parameters:
*
* point - Optional <mxPoint> that specifies the fixed location of the point
* in relative coordinates. Default is null.
* perimeter - Optional boolean that specifies if the fixed point should be
* projected onto the perimeter of the terminal. Default is true.
*/
function mxConnectionConstraint(point, perimeter, name, dx, dy)
{
this.point = point;
this.perimeter = (perimeter != null) ? perimeter : true;
this.name = name;
this.dx = dx? dx : 0;
this.dy = dy? dy : 0;
};
/**
* Variable: point
*
* <mxPoint> that specifies the fixed location of the connection point.
*/
mxConnectionConstraint.prototype.point = null;
/**
* Variable: perimeter
*
* Boolean that specifies if the point should be projected onto the perimeter
* of the terminal.
*/
mxConnectionConstraint.prototype.perimeter = null;
/**
* Variable: name
*
* Optional string that specifies the name of the constraint.
*/
mxConnectionConstraint.prototype.name = null;
/**
* Variable: dx
*
* Optional float that specifies the horizontal offset of the constraint.
*/
mxConnectionConstraint.prototype.dx = null;
/**
* Variable: dy
*
* Optional float that specifies the vertical offset of the constraint.
*/
mxConnectionConstraint.prototype.dy = null;
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGraphHandler
*
* Graph event handler that handles selection. Individual cells are handled
* separately using <mxVertexHandler> or one of the edge handlers. These
* handlers are created using <mxGraph.createHandler> in
* <mxGraphSelectionModel.cellAdded>.
*
* To avoid the container to scroll a moved cell into view, set
* <scrollAfterMove> to false.
*
* Constructor: mxGraphHandler
*
* Constructs an event handler that creates handles for the
* selection cells.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxGraphHandler(graph)
{
this.graph = graph;
this.graph.addMouseListener(this);
// Repaints the handler after autoscroll
this.panHandler = mxUtils.bind(this, function()
{
this.updatePreview();
this.updateHint();
});
this.graph.addListener(mxEvent.PAN, this.panHandler);
// Handles escape keystrokes
this.escapeHandler = mxUtils.bind(this, function(sender, evt)
{
this.reset();
});
this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler);
// Updates the preview box for remote changes
this.refreshHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.first != null)
{
try
{
this.bounds = this.graph.getView().getBounds(this.cells);
this.pBounds = this.getPreviewBounds(this.cells);
this.updatePreview(true);
// Resets handlers after they have been refreshed
window.setTimeout(mxUtils.bind(this, function()
{
if (this.livePreviewUsed)
{
this.setHandlesVisibleForCells(this.cells, false);
this.updatePreview();
}
}), 0);
}
catch (e)
{
// Resets the handler if cells have vanished
this.reset();
}
}
});
this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler);
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxGraphHandler.prototype.graph = null;
/**
* Variable: maxCells
*
* Defines the maximum number of cells to paint subhandles
* for. Default is 50 for Firefox and 20 for IE. Set this
* to 0 if you want an unlimited number of handles to be
* displayed. This is only recommended if the number of
* cells in the graph is limited to a small number, eg.
* 500.
*/
mxGraphHandler.prototype.maxCells = (mxClient.IS_IE) ? 20 : 50;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxGraphHandler.prototype.enabled = true;
/**
* Variable: highlightEnabled
*
* Specifies if drop targets under the mouse should be enabled. Default is
* true.
*/
mxGraphHandler.prototype.highlightEnabled = true;
/**
* Variable: cloneEnabled
*
* Specifies if cloning by control-drag is enabled. Default is true.
*/
mxGraphHandler.prototype.cloneEnabled = true;
/**
* Variable: moveEnabled
*
* Specifies if moving is enabled. Default is true.
*/
mxGraphHandler.prototype.moveEnabled = true;
/**
* Variable: guidesEnabled
*
* Specifies if other cells should be used for snapping the right, center or
* left side of the current selection. Default is false.
*/
mxGraphHandler.prototype.guidesEnabled = false;
/**
* Variable: guide
*
* Holds the <mxGuide> instance that is used for alignment.
*/
mxGraphHandler.prototype.guide = null;
/**
* Variable: currentDx
*
* Stores the x-coordinate of the current mouse move.
*/
mxGraphHandler.prototype.currentDx = null;
/**
* Variable: currentDy
*
* Stores the y-coordinate of the current mouse move.
*/
mxGraphHandler.prototype.currentDy = null;
/**
* Variable: updateCursor
*
* Specifies if a move cursor should be shown if the mouse is over a movable
* cell. Default is true.
*/
mxGraphHandler.prototype.updateCursor = true;
/**
* Variable: selectEnabled
*
* Specifies if selecting is enabled. Default is true.
*/
mxGraphHandler.prototype.selectEnabled = true;
/**
* Variable: removeCellsFromParent
*
* Specifies if cells may be moved out of their parents. Default is true.
*/
mxGraphHandler.prototype.removeCellsFromParent = true;
/**
* Variable: removeEmptyParents
*
* If empty parents should be removed from the model after all child cells
* have been moved out. Default is true.
*/
mxGraphHandler.prototype.removeEmptyParents = false;
/**
* Variable: connectOnDrop
*
* Specifies if drop events are interpreted as new connections if no other
* drop action is defined. Default is false.
*/
mxGraphHandler.prototype.connectOnDrop = false;
/**
* Variable: scrollOnMove
*
* Specifies if the view should be scrolled so that a moved cell is
* visible. Default is true.
*/
mxGraphHandler.prototype.scrollOnMove = true;
/**
* Variable: minimumSize
*
* Specifies the minimum number of pixels for the width and height of a
* selection border. Default is 6.
*/
mxGraphHandler.prototype.minimumSize = 6;
/**
* Variable: previewColor
*
* Specifies the color of the preview shape. Default is black.
*/
mxGraphHandler.prototype.previewColor = 'black';
/**
* Variable: htmlPreview
*
* Specifies if the graph container should be used for preview. If this is used
* then drop target detection relies entirely on <mxGraph.getCellAt> because
* the HTML preview does not "let events through". Default is false.
*/
mxGraphHandler.prototype.htmlPreview = false;
/**
* Variable: shape
*
* Reference to the <mxShape> that represents the preview.
*/
mxGraphHandler.prototype.shape = null;
/**
* Variable: scaleGrid
*
* Specifies if the grid should be scaled. Default is false.
*/
mxGraphHandler.prototype.scaleGrid = false;
/**
* Variable: rotationEnabled
*
* Specifies if the bounding box should allow for rotation. Default is true.
*/
mxGraphHandler.prototype.rotationEnabled = true;
/**
* Variable: maxLivePreview
*
* Maximum number of cells for which live preview should be used. Default is 0
* which means no live preview.
*/
mxGraphHandler.prototype.maxLivePreview = 0;
/**
* Variable: allowLivePreview
*
* If live preview is allowed on this system. Default is true for systems with
* SVG support.
*/
mxGraphHandler.prototype.allowLivePreview = mxClient.IS_SVG;
/**
* Function: isEnabled
*
* Returns <enabled>.
*/
mxGraphHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Sets <enabled>.
*/
mxGraphHandler.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: isCloneEnabled
*
* Returns <cloneEnabled>.
*/
mxGraphHandler.prototype.isCloneEnabled = function()
{
return this.cloneEnabled;
};
/**
* Function: setCloneEnabled
*
* Sets <cloneEnabled>.
*
* Parameters:
*
* value - Boolean that specifies the new clone enabled state.
*/
mxGraphHandler.prototype.setCloneEnabled = function(value)
{
this.cloneEnabled = value;
};
/**
* Function: isMoveEnabled
*
* Returns <moveEnabled>.
*/
mxGraphHandler.prototype.isMoveEnabled = function()
{
return this.moveEnabled;
};
/**
* Function: setMoveEnabled
*
* Sets <moveEnabled>.
*/
mxGraphHandler.prototype.setMoveEnabled = function(value)
{
this.moveEnabled = value;
};
/**
* Function: isSelectEnabled
*
* Returns <selectEnabled>.
*/
mxGraphHandler.prototype.isSelectEnabled = function()
{
return this.selectEnabled;
};
/**
* Function: setSelectEnabled
*
* Sets <selectEnabled>.
*/
mxGraphHandler.prototype.setSelectEnabled = function(value)
{
this.selectEnabled = value;
};
/**
* Function: isRemoveCellsFromParent
*
* Returns <removeCellsFromParent>.
*/
mxGraphHandler.prototype.isRemoveCellsFromParent = function()
{
return this.removeCellsFromParent;
};
/**
* Function: setRemoveCellsFromParent
*
* Sets <removeCellsFromParent>.
*/
mxGraphHandler.prototype.setRemoveCellsFromParent = function(value)
{
this.removeCellsFromParent = value;
};
/**
* Function: getInitialCellForEvent
*
* Hook to return initial cell for the given event.
*/
mxGraphHandler.prototype.getInitialCellForEvent = function(me)
{
return me.getCell();
};
/**
* Function: isDelayedSelection
*
* Hook to return true for delayed selections.
*/
mxGraphHandler.prototype.isDelayedSelection = function(cell, me)
{
return this.graph.isCellSelected(cell);
};
/**
* Function: consumeMouseEvent
*
* Consumes the given mouse event. NOTE: This may be used to enable click
* events for links in labels on iOS as follows as consuming the initial
* touchStart disables firing the subsequent click event on the link.
*
* <code>
* mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me)
* {
* var source = mxEvent.getSource(me.getEvent());
*
* if (!mxEvent.isTouchEvent(me.getEvent()) || source.nodeName != 'A')
* {
* me.consume();
* }
* }
* </code>
*/
mxGraphHandler.prototype.consumeMouseEvent = function(evtName, me)
{
me.consume();
};
/**
* Function: mouseDown
*
* Handles the event by selecing the given cell and creating a handle for
* it. By consuming the event all subsequent events of the gesture are
* redirected to this handler.
*/
mxGraphHandler.prototype.mouseDown = function(sender, me)
{
if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() &&
me.getState() != null && !mxEvent.isMultiTouchEvent(me.getEvent()))
{
var cell = this.getInitialCellForEvent(me);
this.delayedSelection = this.isDelayedSelection(cell, me);
this.cell = null;
if (this.isSelectEnabled() && !this.delayedSelection)
{
this.graph.selectCellForEvent(cell, me.getEvent());
}
if (this.isMoveEnabled())
{
var model = this.graph.model;
var geo = model.getGeometry(cell);
if (this.graph.isCellMovable(cell) && ((!model.isEdge(cell) || this.graph.getSelectionCount() > 1 ||
(geo.points != null && geo.points.length > 0) || model.getTerminal(cell, true) == null ||
model.getTerminal(cell, false) == null) || this.graph.allowDanglingEdges ||
(this.graph.isCloneEvent(me.getEvent()) && this.graph.isCellsCloneable())))
{
this.start(cell, me.getX(), me.getY());
}
else if (this.delayedSelection)
{
this.cell = cell;
}
this.cellWasClicked = true;
this.consumeMouseEvent(mxEvent.MOUSE_DOWN, me);
}
}
};
/**
* Function: getGuideStates
*
* Creates an array of cell states which should be used as guides.
*/
mxGraphHandler.prototype.getGuideStates = function()
{
var parent = this.graph.getDefaultParent();
var model = this.graph.getModel();
var filter = mxUtils.bind(this, function(cell)
{
return this.graph.view.getState(cell) != null &&
model.isVertex(cell) &&
model.getGeometry(cell) != null &&
!model.getGeometry(cell).relative;
});
return this.graph.view.getCellStates(model.filterDescendants(filter, parent));
};
/**
* Function: getCells
*
* Returns the cells to be modified by this handler. This implementation
* returns all selection cells that are movable, or the given initial cell if
* the given cell is not selected and movable. This handles the case of moving
* unselectable or unselected cells.
*
* Parameters:
*
* initialCell - <mxCell> that triggered this handler.
*/
mxGraphHandler.prototype.getCells = function(initialCell)
{
if (!this.delayedSelection && this.graph.isCellMovable(initialCell))
{
return [initialCell];
}
else
{
return this.graph.getMovableCells(this.graph.getSelectionCells());
}
};
/**
* Function: getPreviewBounds
*
* Returns the <mxRectangle> used as the preview bounds for
* moving the given cells.
*/
mxGraphHandler.prototype.getPreviewBounds = function(cells)
{
var bounds = this.getBoundingBox(cells);
if (bounds != null)
{
// Corrects width and height
bounds.width = Math.max(0, bounds.width - 1);
bounds.height = Math.max(0, bounds.height - 1);
if (bounds.width < this.minimumSize)
{
var dx = this.minimumSize - bounds.width;
bounds.x -= dx / 2;
bounds.width = this.minimumSize;
}
else
{
bounds.x = Math.round(bounds.x);
bounds.width = Math.ceil(bounds.width);
}
var tr = this.graph.view.translate;
var s = this.graph.view.scale;
if (bounds.height < this.minimumSize)
{
var dy = this.minimumSize - bounds.height;
bounds.y -= dy / 2;
bounds.height = this.minimumSize;
}
else
{
bounds.y = Math.round(bounds.y);
bounds.height = Math.ceil(bounds.height);
}
}
return bounds;
};
/**
* Function: getBoundingBox
*
* Returns the union of the <mxCellStates> for the given array of <mxCells>.
* For vertices, this method uses the bounding box of the corresponding shape
* if one exists. The bounding box of the corresponding text label and all
* controls and overlays are ignored. See also: <mxGraphView.getBounds> and
* <mxGraph.getBoundingBox>.
*
* Parameters:
*
* cells - Array of <mxCells> whose bounding box should be returned.
*/
mxGraphHandler.prototype.getBoundingBox = function(cells)
{
var result = null;
if (cells != null && cells.length > 0)
{
var model = this.graph.getModel();
for (var i = 0; i < cells.length; i++)
{
if (model.isVertex(cells[i]) || model.isEdge(cells[i]))
{
var state = this.graph.view.getState(cells[i]);
if (state != null)
{
var bbox = state;
if (model.isVertex(cells[i]) && state.shape != null && state.shape.boundingBox != null)
{
bbox = state.shape.boundingBox;
}
if (result == null)
{
result = mxRectangle.fromRectangle(bbox);
}
else
{
result.add(bbox);
}
}
}
}
}
return result;
};
/**
* Function: createPreviewShape
*
* Creates the shape used to draw the preview for the given bounds.
*/
mxGraphHandler.prototype.createPreviewShape = function(bounds)
{
var shape = new mxRectangleShape(bounds, null, this.previewColor);
shape.isDashed = true;
if (this.htmlPreview)
{
shape.dialect = mxConstants.DIALECT_STRICTHTML;
shape.init(this.graph.container);
}
else
{
// Makes sure to use either VML or SVG shapes in order to implement
// event-transparency on the background area of the rectangle since
// HTML shapes do not let mouseevents through even when transparent
shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
shape.init(this.graph.getView().getOverlayPane());
shape.pointerEvents = false;
// Workaround for artifacts on iOS
if (mxClient.IS_IOS)
{
shape.getSvgScreenOffset = function()
{
return 0;
};
}
}
return shape;
};
/**
* Function: start
*
* Starts the handling of the mouse gesture.
*/
mxGraphHandler.prototype.start = function(cell, x, y)
{
this.cell = cell;
this.first = mxUtils.convertPoint(this.graph.container, x, y);
this.cells = this.getCells(this.cell);
this.bounds = this.graph.getView().getBounds(this.cells);
this.pBounds = this.getPreviewBounds(this.cells);
this.allCells = new mxDictionary();
this.cloning = false;
this.cellCount = 0;
for (var i = 0; i < this.cells.length; i++)
{
this.cellCount += this.addStates(this.cells[i], this.allCells);
}
if (this.guidesEnabled)
{
this.guide = new mxGuide(this.graph, this.getGuideStates());
var parent = this.graph.model.getParent(cell);
var ignore = this.graph.model.getChildCount(parent) < 2;
this.guide.isStateIgnored = mxUtils.bind(this, function(state)
{
var p = this.graph.model.getParent(state.cell);
return state.cell != null && ((!this.cloning &&
this.isCellMoving(state.cell)) ||
(state.cell != (this.target || parent) && !ignore &&
(this.target == null || this.graph.model.getChildCount(
this.target) >= 2) && p != (this.target || parent)));
});
}
};
/**
* Function: addStates
*
* Adds the states for the given cell recursively to the given dictionary.
*/
mxGraphHandler.prototype.addStates = function(cell, dict)
{
var state = this.graph.view.getState(cell);
var count = 0;
if (state != null && dict.get(cell) == null)
{
dict.put(cell, state);
count++;
var childCount = this.graph.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
count += this.addStates(this.graph.model.getChildAt(cell, i), dict);
}
}
return count;
};
/**
* Function: isCellMoving
*
* Returns true if the given cell is currently being moved.
*/
mxGraphHandler.prototype.isCellMoving = function(cell)
{
return this.allCells.get(cell) != null;
};
/**
* Function: useGuidesForEvent
*
* Returns true if the guides should be used for the given <mxMouseEvent>.
* This implementation returns <mxGuide.isEnabledForEvent>.
*/
mxGraphHandler.prototype.useGuidesForEvent = function(me)
{
return (this.guide != null) ? this.guide.isEnabledForEvent(me.getEvent()) : true;
};
/**
* Function: snap
*
* Snaps the given vector to the grid and returns the given mxPoint instance.
*/
mxGraphHandler.prototype.snap = function(vector)
{
var scale = (this.scaleGrid) ? this.graph.view.scale : 1;
vector.x = this.graph.snap(vector.x / scale) * scale;
vector.y = this.graph.snap(vector.y / scale) * scale;
return vector;
};
/**
* Function: getDelta
*
* Returns an <mxPoint> that represents the vector for moving the cells
* for the given <mxMouseEvent>.
*/
mxGraphHandler.prototype.getDelta = function(me)
{
var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY());
var s = this.graph.view.scale;
return new mxPoint(this.roundLength((point.x - this.first.x - this.graph.panDx) / s) * s,
this.roundLength((point.y - this.first.y - this.graph.panDy) / s) * s);
};
/**
* Function: updateHint
*
* Hook for subclassers do show details while the handler is active.
*/
mxGraphHandler.prototype.updateHint = function(me) { };
/**
* Function: removeHint
*
* Hooks for subclassers to hide details when the handler gets inactive.
*/
mxGraphHandler.prototype.removeHint = function() { };
/**
* Function: roundLength
*
* Hook for rounding the unscaled vector. This uses Math.round.
*/
mxGraphHandler.prototype.roundLength = function(length)
{
return Math.round(length * 2) / 2;
};
/**
* Function: mouseMove
*
* Handles the event by highlighting possible drop targets and updating the
* preview.
*/
mxGraphHandler.prototype.mouseMove = function(sender, me)
{
var graph = this.graph;
if (!me.isConsumed() && graph.isMouseDown && this.cell != null &&
this.first != null && this.bounds != null)
{
// Stops moving if a multi touch event is received
if (mxEvent.isMultiTouchEvent(me.getEvent()))
{
this.reset();
return;
}
var delta = this.getDelta(me);
var dx = delta.x;
var dy = delta.y;
var tol = graph.tolerance;
if (this.shape != null || this.livePreviewActive || Math.abs(dx) > tol || Math.abs(dy) > tol)
{
// Highlight is used for highlighting drop targets
if (this.highlight == null)
{
this.highlight = new mxCellHighlight(this.graph,
mxConstants.DROP_TARGET_COLOR, 3);
}
var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled();
var gridEnabled = graph.isGridEnabledEvent(me.getEvent());
var cell = me.getCell();
var hideGuide = true;
var target = null;
this.cloning = clone;
if (graph.isDropEnabled() && this.highlightEnabled)
{
// Contains a call to getCellAt to find the cell under the mouse
target = graph.getDropTarget(this.cells, me.getEvent(), cell, clone);
}
var state = graph.getView().getState(target);
var highlight = false;
if (state != null && (graph.model.getParent(this.cell) != target || clone))
{
if (this.target != target)
{
this.target = target;
this.setHighlightColor(mxConstants.DROP_TARGET_COLOR);
}
highlight = true;
}
else
{
this.target = null;
if (this.connectOnDrop && cell != null && this.cells.length == 1 &&
graph.getModel().isVertex(cell) && graph.isCellConnectable(cell))
{
state = graph.getView().getState(cell);
if (state != null)
{
var error = graph.getEdgeValidationError(null, this.cell, cell);
var color = (error == null) ?
mxConstants.VALID_COLOR :
mxConstants.INVALID_CONNECT_TARGET_COLOR;
this.setHighlightColor(color);
highlight = true;
}
}
}
if (state != null && highlight)
{
this.highlight.highlight(state);
}
else
{
this.highlight.hide();
}
if (this.livePreviewActive && clone)
{
this.resetLivePreview();
this.livePreviewActive = false;
}
else if (this.maxLivePreview >= this.cellCount && !this.livePreviewActive && this.allowLivePreview)
{
this.setHandlesVisibleForCells(this.cells, false);
this.livePreviewActive = true;
this.livePreviewUsed = true;
}
else if (!this.livePreviewUsed && this.shape == null)
{
this.shape = this.createPreviewShape(this.bounds);
}
if (this.guide != null && this.useGuidesForEvent(me))
{
delta = this.guide.move(this.bounds, new mxPoint(dx, dy), gridEnabled, clone);
hideGuide = false;
dx = delta.x;
dy = delta.y;
}
else if (gridEnabled)
{
var trx = graph.getView().translate;
var scale = graph.getView().scale;
var tx = this.bounds.x - (graph.snap(this.bounds.x / scale - trx.x) + trx.x) * scale;
var ty = this.bounds.y - (graph.snap(this.bounds.y / scale - trx.y) + trx.y) * scale;
var v = this.snap(new mxPoint(dx, dy));
dx = v.x - tx;
dy = v.y - ty;
}
if (this.guide != null && hideGuide)
{
this.guide.hide();
}
// Constrained movement if shift key is pressed
if (graph.isConstrainedEvent(me.getEvent()))
{
if (Math.abs(dx) > Math.abs(dy))
{
dy = 0;
}
else
{
dx = 0;
}
}
this.currentDx = dx;
this.currentDy = dy;
this.updatePreview();
}
this.updateHint(me);
this.consumeMouseEvent(mxEvent.MOUSE_MOVE, me);
// Cancels the bubbling of events to the container so
// that the droptarget is not reset due to an mouseMove
// fired on the container with no associated state.
mxEvent.consume(me.getEvent());
}
else if ((this.isMoveEnabled() || this.isCloneEnabled()) && this.updateCursor && !me.isConsumed() &&
(me.getState() != null || me.sourceState != null) && !graph.isMouseDown)
{
var cursor = graph.getCursorForMouseEvent(me);
if (cursor == null && graph.isEnabled() && graph.isCellMovable(me.getCell()))
{
if (graph.getModel().isEdge(me.getCell()))
{
cursor = mxConstants.CURSOR_MOVABLE_EDGE;
}
else
{
cursor = mxConstants.CURSOR_MOVABLE_VERTEX;
}
}
// Sets the cursor on the original source state under the mouse
// instead of the event source state which can be the parent
if (cursor != null && me.sourceState != null)
{
me.sourceState.setCursor(cursor);
}
}
};
/**
* Function: updatePreview
*
* Updates the bounds of the preview shape.
*/
mxGraphHandler.prototype.updatePreview = function(remote)
{
if (this.livePreviewUsed && !remote)
{
if (this.cells != null)
{
this.updateLivePreview(this.currentDx, this.currentDy);
}
}
else
{
this.updatePreviewShape();
}
};
/**
* Function: updatePreviewShape
*
* Updates the bounds of the preview shape.
*/
mxGraphHandler.prototype.updatePreviewShape = function()
{
if (this.shape != null)
{
this.shape.bounds = new mxRectangle(Math.round(this.pBounds.x + this.currentDx),
Math.round(this.pBounds.y + this.currentDy), this.pBounds.width, this.pBounds.height);
this.shape.redraw();
}
};
/**
* Function: updateLivePreview
*
* Updates the bounds of the preview shape.
*/
mxGraphHandler.prototype.updateLivePreview = function(dx, dy)
{
var states = [];
if (this.allCells != null)
{
this.allCells.visit(mxUtils.bind(this, function(key, state)
{
// Saves current state
var tempState = state.clone();
states.push([state, tempState]);
// Makes transparent for events to detect drop targets
if (state.shape != null)
{
if (state.shape.originalPointerEvents == null)
{
state.shape.originalPointerEvents = state.shape.pointerEvents;
}
state.shape.pointerEvents = false;
if (state.text != null && state.text.node != null)
{
var node = state.text.node;
if (node.firstChild != null && node.firstChild.firstChild != null &&
node.firstChild.firstChild.nodeName == 'foreignObject')
{
node.firstChild.firstChild.setAttribute('pointer-events', 'none');
}
else if (node.ownerSVGElement != null)
{
node.setAttribute('pointer-events', 'none');
}
else
{
node.style.pointerEvents = 'none';
}
}
}
// Temporarily changes position
if (this.graph.model.isVertex(state.cell))
{
state.x += dx;
state.y += dy;
// Draws the live preview
if (!this.cloning)
{
state.view.graph.cellRenderer.redraw(state, true);
// Forces redraw of connected edges after all states
// have been updated but avoids update of state
state.view.invalidate(state.cell);
state.invalid = false;
}
// Hides folding icon
if (state.control != null && state.control.node != null)
{
state.control.node.style.visibility = 'hidden';
}
}
}));
}
// Redraws connected edges
var s = this.graph.view.scale;
for (var i = 0; i < states.length; i++)
{
var state = states[i][0];
if (this.graph.model.isEdge(state.cell))
{
var geometry = this.graph.getCellGeometry(state.cell);
var points = [];
if (geometry != null && geometry.points != null)
{
for (var j = 0; j < geometry.points.length; j++)
{
if (geometry.points[j] != null)
{
points.push(new mxPoint(
geometry.points[j].x + dx / s,
geometry.points[j].y + dy / s));
}
}
}
var source = state.visibleSourceState;
var target = state.visibleTargetState;
var pts = states[i][1].absolutePoints;
if (source == null || !this.isCellMoving(source.cell))
{
var pt0 = pts[0];
state.setAbsoluteTerminalPoint(new mxPoint(pt0.x + dx, pt0.y + dy), true);
source = null;
}
else
{
state.view.updateFixedTerminalPoint(state, source, true,
this.graph.getConnectionConstraint(state, source, true));
}
if (target == null || !this.isCellMoving(target.cell))
{
var ptn = pts[pts.length - 1];
state.setAbsoluteTerminalPoint(new mxPoint(ptn.x + dx, ptn.y + dy), false);
target = null;
}
else
{
state.view.updateFixedTerminalPoint(state, target, false,
this.graph.getConnectionConstraint(state, target, false));
}
state.view.updatePoints(state, points, source, target);
state.view.updateFloatingTerminalPoints(state, source, target);
state.invalid = false;
// Draws the live preview but avoids update of state
if (!this.cloning)
{
state.view.graph.cellRenderer.redraw(state, true);
}
}
}
this.graph.view.validate();
this.redrawHandles(states);
this.resetPreviewStates(states);
};
/**
* Function: redrawHandles
*
* Redraws the preview shape for the given states array.
*/
mxGraphHandler.prototype.redrawHandles = function(states)
{
for (var i = 0; i < states.length; i++)
{
var handler = this.graph.selectionCellsHandler.getHandler(states[i][0].cell);
if (handler != null)
{
handler.redraw(true);
}
}
};
/**
* Function: resetPreviewStates
*
* Resets the given preview states array.
*/
mxGraphHandler.prototype.resetPreviewStates = function(states)
{
for (var i = 0; i < states.length; i++)
{
states[i][0].setState(states[i][1]);
}
};
/**
* Function: resetLivePreview
*
* Resets the livew preview.
*/
mxGraphHandler.prototype.resetLivePreview = function()
{
if (this.allCells != null)
{
this.allCells.visit(mxUtils.bind(this, function(key, state)
{
// Restores event handling
if (state.shape != null && state.shape.originalPointerEvents != null)
{
state.shape.pointerEvents = state.shape.originalPointerEvents;
state.shape.originalPointerEvents = null;
// Forces a repaint event if not moved
state.shape.bounds = null;
if (state.text != null && state.text.node != null)
{
var node = state.text.node;
if (node.firstChild != null && node.firstChild.firstChild != null &&
node.firstChild.firstChild.nodeName == 'foreignObject')
{
node.firstChild.firstChild.setAttribute('pointer-events', 'all');
}
else if (node.ownerSVGElement != null)
{
node.removeAttribute('pointer-events');
}
else
{
node.style.pointerEvents = '';
}
}
}
// Shows folding icon
if (state.control != null && state.control.node != null)
{
state.control.node.style.visibility = '';
}
// Forces repaint of state and connected edges
state.view.invalidate(state.cell);
}));
// Repaints all invalid states
this.graph.view.validate();
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxGraphHandler.prototype.setHandlesVisibleForCells = function(cells, visible)
{
for (var i = 0; i < cells.length; i++)
{
var cell = cells[i];
var handler = this.graph.selectionCellsHandler.getHandler(cell);
if (handler != null)
{
handler.setHandlesVisible(visible);
if (visible)
{
handler.redraw();
}
}
}
};
/**
* Function: setHighlightColor
*
* Sets the color of the rectangle used to highlight drop targets.
*
* Parameters:
*
* color - String that represents the new highlight color.
*/
mxGraphHandler.prototype.setHighlightColor = function(color)
{
if (this.highlight != null)
{
this.highlight.setHighlightColor(color);
}
};
/**
* Function: mouseUp
*
* Handles the event by applying the changes to the selection cells.
*/
mxGraphHandler.prototype.mouseUp = function(sender, me)
{
if (!me.isConsumed())
{
if (this.livePreviewUsed)
{
this.resetLivePreview();
}
if (this.cell != null && this.first != null && (this.shape != null || this.livePreviewUsed) &&
this.currentDx != null && this.currentDy != null)
{
var graph = this.graph;
var cell = me.getCell();
if (this.connectOnDrop && this.target == null && cell != null && graph.getModel().isVertex(cell) &&
graph.isCellConnectable(cell) && graph.isEdgeValid(null, this.cell, cell))
{
graph.connectionHandler.connect(this.cell, cell, me.getEvent());
}
else
{
var clone = graph.isCloneEvent(me.getEvent()) && graph.isCellsCloneable() && this.isCloneEnabled();
var scale = graph.getView().scale;
var dx = this.roundLength(this.currentDx / scale);
var dy = this.roundLength(this.currentDy / scale);
var target = this.target;
if (graph.isSplitEnabled() && graph.isSplitTarget(target, this.cells, me.getEvent()))
{
graph.splitEdge(target, this.cells, null, dx, dy);
}
else
{
this.moveCells(this.cells, dx, dy, clone, this.target, me.getEvent());
}
}
}
else if (this.isSelectEnabled() && this.delayedSelection && this.cell != null)
{
this.selectDelayed(me);
}
}
// Consumes the event if a cell was initially clicked
if (this.cellWasClicked)
{
this.consumeMouseEvent(mxEvent.MOUSE_UP, me);
}
this.reset();
};
/**
* Function: selectDelayed
*
* Implements the delayed selection for the given mouse event.
*/
mxGraphHandler.prototype.selectDelayed = function(me)
{
if (!this.graph.isCellSelected(this.cell) || !this.graph.popupMenuHandler.isPopupTrigger(me))
{
this.graph.selectCellForEvent(this.cell, me.getEvent());
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxGraphHandler.prototype.reset = function()
{
if (this.livePreviewUsed)
{
this.resetLivePreview();
this.setHandlesVisibleForCells(this.cells, true);
}
this.destroyShapes();
this.removeHint();
this.delayedSelection = false;
this.livePreviewActive = null;
this.livePreviewUsed = null;
this.cellWasClicked = false;
this.currentDx = null;
this.currentDy = null;
this.cellCount = null;
this.cloning = false;
this.allCells = null;
this.guides = null;
this.target = null;
this.first = null;
this.cells = null;
this.cell = null;
};
/**
* Function: shouldRemoveCellsFromParent
*
* Returns true if the given cells should be removed from the parent for the specified
* mousereleased event.
*/
mxGraphHandler.prototype.shouldRemoveCellsFromParent = function(parent, cells, evt)
{
if (this.graph.getModel().isVertex(parent))
{
var pState = this.graph.getView().getState(parent);
if (pState != null)
{
var pt = mxUtils.convertPoint(this.graph.container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
var alpha = mxUtils.toRadians(mxUtils.getValue(pState.style, mxConstants.STYLE_ROTATION) || 0);
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var cx = new mxPoint(pState.getCenterX(), pState.getCenterY());
pt = mxUtils.getRotatedPoint(pt, cos, sin, cx);
}
return !mxUtils.contains(pState, pt.x, pt.y);
}
}
return false;
};
/**
* Function: moveCells
*
* Moves the given cells by the specified amount.
*/
mxGraphHandler.prototype.moveCells = function(cells, dx, dy, clone, target, evt)
{
if (clone)
{
cells = this.graph.getCloneableCells(cells);
}
// Removes cells from parent
var parent = this.graph.getModel().getParent(this.cell);
if (target == null && this.isRemoveCellsFromParent() &&
this.shouldRemoveCellsFromParent(parent, cells, evt))
{
target = this.graph.getDefaultParent();
}
// Cloning into locked cells is not allowed
clone = clone && !this.graph.isCellLocked(target || this.graph.getDefaultParent());
this.graph.getModel().beginUpdate();
try
{
var parents = [];
// Removes parent if all child cells are removed
if (!clone && target != null && this.removeEmptyParents)
{
// Collects all non-selected parents
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
// LATER: Recurse up the cell hierarchy
for (var i = 0; i < cells.length; i++)
{
var par = this.graph.model.getParent(cells[i]);
if (par != null && !dict.get(par))
{
dict.put(par, true);
parents.push(par);
}
}
}
// Passes all selected cells in order to correctly clone or move into
// the target cell. The method checks for each cell if its movable.
cells = this.graph.moveCells(cells, dx, dy, clone, target, evt);
// Removes parent if all child cells are removed
var temp = [];
for (var i = 0; i < parents.length; i++)
{
if (this.shouldRemoveParent(parents[i]))
{
temp.push(parents[i]);
}
}
this.graph.removeCells(temp, false);
}
finally
{
this.graph.getModel().endUpdate();
}
// Selects the new cells if cells have been cloned
if (clone)
{
this.graph.setSelectionCells(cells);
}
if (this.isSelectEnabled() && this.scrollOnMove)
{
this.graph.scrollCellToVisible(cells[0]);
}
};
/**
* Function: moveCells
*
* Moves the given cells by the specified amount.
*/
mxGraphHandler.prototype.shouldRemoveParent = function(parent)
{
var state = this.graph.view.getState(parent);
if (state != null && (this.graph.model.isEdge(state.cell) || this.graph.model.isVertex(state.cell)) &&
this.graph.isCellDeletable(state.cell) && this.graph.model.getChildCount(state.cell) == 0)
{
var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE);
var fill = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE);
return stroke == mxConstants.NONE && fill == mxConstants.NONE;
}
return false;
};
/**
* Function: destroyShapes
*
* Destroy the preview and highlight shapes.
*/
mxGraphHandler.prototype.destroyShapes = function()
{
// Destroys the preview dashed rectangle
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
if (this.guide != null)
{
this.guide.destroy();
this.guide = null;
}
// Destroys the drop target highlight
if (this.highlight != null)
{
this.highlight.destroy();
this.highlight = null;
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxGraphHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
this.graph.removeListener(this.panHandler);
if (this.escapeHandler != null)
{
this.graph.removeListener(this.escapeHandler);
this.escapeHandler = null;
}
if (this.refreshHandler != null)
{
this.graph.getModel().removeListener(this.refreshHandler);
this.refreshHandler = null;
}
this.destroyShapes();
this.removeHint();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPanningHandler
*
* Event handler that pans and creates popupmenus. To use the left
* mousebutton for panning without interfering with cell moving and
* resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size
* steps while panning, use <useGrid>. This handler is built-into
* <mxGraph.panningHandler> and enabled using <mxGraph.setPanning>.
*
* Constructor: mxPanningHandler
*
* Constructs an event handler that creates a <mxPopupMenu>
* and pans the graph.
*
* Event: mxEvent.PAN_START
*
* Fires when the panning handler changes its <active> state to true. The
* <code>event</code> property contains the corresponding <mxMouseEvent>.
*
* Event: mxEvent.PAN
*
* Fires while handle is processing events. The <code>event</code> property contains
* the corresponding <mxMouseEvent>.
*
* Event: mxEvent.PAN_END
*
* Fires when the panning handler changes its <active> state to false. The
* <code>event</code> property contains the corresponding <mxMouseEvent>.
*/
function mxPanningHandler(graph)
{
if (graph != null)
{
this.graph = graph;
this.graph.addMouseListener(this);
// Handles force panning event
this.forcePanningHandler = mxUtils.bind(this, function(sender, evt)
{
var evtName = evt.getProperty('eventName');
var me = evt.getProperty('event');
if (evtName == mxEvent.MOUSE_DOWN && this.isForcePanningEvent(me))
{
this.start(me);
this.active = true;
this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me));
me.consume();
}
});
this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forcePanningHandler);
// Handles pinch gestures
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
if (this.isPinchEnabled())
{
var evt = eo.getProperty('event');
if (!mxEvent.isConsumed(evt) && evt.type == 'gesturestart')
{
this.initialScale = this.graph.view.scale;
// Forces start of panning when pinch gesture starts
if (!this.active && this.mouseDownEvent != null)
{
this.start(this.mouseDownEvent);
this.mouseDownEvent = null;
}
}
else if (evt.type == 'gestureend' && this.initialScale != null)
{
this.initialScale = null;
}
if (this.initialScale != null)
{
var value = Math.round(this.initialScale * evt.scale * 100) / 100;
if (this.minScale != null)
{
value = Math.max(this.minScale, value);
}
if (this.maxScale != null)
{
value = Math.min(this.maxScale, value);
}
if (this.graph.view.scale != value)
{
this.graph.zoomTo(value);
mxEvent.consume(evt);
}
}
}
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
this.mouseUpListener = mxUtils.bind(this, function()
{
if (this.active)
{
this.reset();
}
});
// Stops scrolling on every mouseup anywhere in the document
mxEvent.addListener(document, 'mouseup', this.mouseUpListener);
}
};
/**
* Extends mxEventSource.
*/
mxPanningHandler.prototype = new mxEventSource();
mxPanningHandler.prototype.constructor = mxPanningHandler;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxPanningHandler.prototype.graph = null;
/**
* Variable: useLeftButtonForPanning
*
* Specifies if panning should be active for the left mouse button.
* Setting this to true may conflict with <mxRubberband>. Default is false.
*/
mxPanningHandler.prototype.useLeftButtonForPanning = false;
/**
* Variable: usePopupTrigger
*
* Specifies if <mxEvent.isPopupTrigger> should also be used for panning.
*/
mxPanningHandler.prototype.usePopupTrigger = true;
/**
* Variable: ignoreCell
*
* Specifies if panning should be active even if there is a cell under the
* mousepointer. Default is false.
*/
mxPanningHandler.prototype.ignoreCell = false;
/**
* Variable: previewEnabled
*
* Specifies if the panning should be previewed. Default is true.
*/
mxPanningHandler.prototype.previewEnabled = true;
/**
* Variable: useGrid
*
* Specifies if the panning steps should be aligned to the grid size.
* Default is false.
*/
mxPanningHandler.prototype.useGrid = false;
/**
* Variable: panningEnabled
*
* Specifies if panning should be enabled. Default is true.
*/
mxPanningHandler.prototype.panningEnabled = true;
/**
* Variable: pinchEnabled
*
* Specifies if pinch gestures should be handled as zoom. Default is true.
*/
mxPanningHandler.prototype.pinchEnabled = true;
/**
* Variable: maxScale
*
* Specifies the maximum scale. Default is 8.
*/
mxPanningHandler.prototype.maxScale = 8;
/**
* Variable: minScale
*
* Specifies the minimum scale. Default is 0.01.
*/
mxPanningHandler.prototype.minScale = 0.01;
/**
* Variable: dx
*
* Holds the current horizontal offset.
*/
mxPanningHandler.prototype.dx = null;
/**
* Variable: dy
*
* Holds the current vertical offset.
*/
mxPanningHandler.prototype.dy = null;
/**
* Variable: startX
*
* Holds the x-coordinate of the start point.
*/
mxPanningHandler.prototype.startX = 0;
/**
* Variable: startY
*
* Holds the y-coordinate of the start point.
*/
mxPanningHandler.prototype.startY = 0;
/**
* Function: isActive
*
* Returns true if the handler is currently active.
*/
mxPanningHandler.prototype.isActive = function()
{
return this.active || this.initialScale != null;
};
/**
* Function: isPanningEnabled
*
* Returns <panningEnabled>.
*/
mxPanningHandler.prototype.isPanningEnabled = function()
{
return this.panningEnabled;
};
/**
* Function: setPanningEnabled
*
* Sets <panningEnabled>.
*/
mxPanningHandler.prototype.setPanningEnabled = function(value)
{
this.panningEnabled = value;
};
/**
* Function: isPinchEnabled
*
* Returns <pinchEnabled>.
*/
mxPanningHandler.prototype.isPinchEnabled = function()
{
return this.pinchEnabled;
};
/**
* Function: setPinchEnabled
*
* Sets <pinchEnabled>.
*/
mxPanningHandler.prototype.setPinchEnabled = function(value)
{
this.pinchEnabled = value;
};
/**
* Function: isPanningTrigger
*
* Returns true if the given event is a panning trigger for the optional
* given cell. This returns true if control-shift is pressed or if
* <usePopupTrigger> is true and the event is a popup trigger.
*/
mxPanningHandler.prototype.isPanningTrigger = function(me)
{
var evt = me.getEvent();
return (this.useLeftButtonForPanning && me.getState() == null &&
mxEvent.isLeftMouseButton(evt)) || (mxEvent.isControlDown(evt) &&
mxEvent.isShiftDown(evt)) || (this.usePopupTrigger && mxEvent.isPopupTrigger(evt));
};
/**
* Function: isForcePanningEvent
*
* Returns true if the given <mxMouseEvent> should start panning. This
* implementation always returns true if <ignoreCell> is true or for
* multi touch events.
*/
mxPanningHandler.prototype.isForcePanningEvent = function(me)
{
return this.ignoreCell || mxEvent.isMultiTouchEvent(me.getEvent());
};
/**
* Function: mouseDown
*
* Handles the event by initiating the panning. By consuming the event all
* subsequent events of the gesture are redirected to this handler.
*/
mxPanningHandler.prototype.mouseDown = function(sender, me)
{
this.mouseDownEvent = me;
if (!me.isConsumed() && this.isPanningEnabled() && !this.active && this.isPanningTrigger(me))
{
this.start(me);
this.consumePanningTrigger(me);
}
};
/**
* Function: start
*
* Starts panning at the given event.
*/
mxPanningHandler.prototype.start = function(me)
{
this.dx0 = -this.graph.container.scrollLeft;
this.dy0 = -this.graph.container.scrollTop;
// Stores the location of the trigger event
this.startX = me.getX();
this.startY = me.getY();
this.dx = null;
this.dy = null;
this.panningTrigger = true;
};
/**
* Function: consumePanningTrigger
*
* Consumes the given <mxMouseEvent> if it was a panning trigger in
* <mouseDown>. The default is to invoke <mxMouseEvent.consume>. Note that this
* will block any further event processing. If you haven't disabled built-in
* context menus and require immediate selection of the cell on mouseDown in
* Safari and/or on the Mac, then use the following code:
*
* (code)
* mxPanningHandler.prototype.consumePanningTrigger = function(me)
* {
* if (me.evt.preventDefault)
* {
* me.evt.preventDefault();
* }
*
* // Stops event processing in IE
* me.evt.returnValue = false;
*
* // Sets local consumed state
* if (!mxClient.IS_SF && !mxClient.IS_MAC)
* {
* me.consumed = true;
* }
* };
* (end)
*/
mxPanningHandler.prototype.consumePanningTrigger = function(me)
{
me.consume();
};
/**
* Function: mouseMove
*
* Handles the event by updating the panning on the graph.
*/
mxPanningHandler.prototype.mouseMove = function(sender, me)
{
this.dx = me.getX() - this.startX;
this.dy = me.getY() - this.startY;
if (this.active)
{
if (this.previewEnabled)
{
// Applies the grid to the panning steps
if (this.useGrid)
{
this.dx = this.graph.snap(this.dx);
this.dy = this.graph.snap(this.dy);
}
this.graph.panGraph(this.dx + this.dx0, this.dy + this.dy0);
}
this.fireEvent(new mxEventObject(mxEvent.PAN, 'event', me));
}
else if (this.panningTrigger)
{
var tmp = this.active;
// Panning is activated only if the mouse is moved
// beyond the graph tolerance
this.active = Math.abs(this.dx) > this.graph.tolerance || Math.abs(this.dy) > this.graph.tolerance;
if (!tmp && this.active)
{
this.fireEvent(new mxEventObject(mxEvent.PAN_START, 'event', me));
}
}
if (this.active || this.panningTrigger)
{
me.consume();
}
};
/**
* Function: mouseUp
*
* Handles the event by setting the translation on the view or showing the
* popupmenu.
*/
mxPanningHandler.prototype.mouseUp = function(sender, me)
{
if (this.active)
{
if (this.dx != null && this.dy != null)
{
// Ignores if scrollbars have been used for panning
if (!this.graph.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.graph.container))
{
var scale = this.graph.getView().scale;
var t = this.graph.getView().translate;
this.graph.panGraph(0, 0);
this.panGraph(t.x + this.dx / scale, t.y + this.dy / scale);
}
me.consume();
}
this.fireEvent(new mxEventObject(mxEvent.PAN_END, 'event', me));
}
this.reset();
};
/**
* Function: mouseUp
*
* Handles the event by setting the translation on the view or showing the
* popupmenu.
*/
mxPanningHandler.prototype.reset = function()
{
this.panningTrigger = false;
this.mouseDownEvent = null;
this.active = false;
this.dx = null;
this.dy = null;
};
/**
* Function: panGraph
*
* Pans <graph> by the given amount.
*/
mxPanningHandler.prototype.panGraph = function(dx, dy)
{
this.graph.getView().setTranslate(dx, dy);
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPanningHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
this.graph.removeListener(this.forcePanningHandler);
this.graph.removeListener(this.gestureHandler);
mxEvent.removeListener(document, 'mouseup', this.mouseUpListener);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPopupMenuHandler
*
* Event handler that creates popupmenus.
*
* Constructor: mxPopupMenuHandler
*
* Constructs an event handler that creates a <mxPopupMenu>.
*/
function mxPopupMenuHandler(graph, factoryMethod)
{
if (graph != null)
{
this.graph = graph;
this.factoryMethod = factoryMethod;
this.graph.addMouseListener(this);
// Does not show menu if any touch gestures take place after the trigger
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
this.inTolerance = false;
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
this.init();
}
};
/**
* Extends mxPopupMenu.
*/
mxPopupMenuHandler.prototype = new mxPopupMenu();
mxPopupMenuHandler.prototype.constructor = mxPopupMenuHandler;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxPopupMenuHandler.prototype.graph = null;
/**
* Variable: selectOnPopup
*
* Specifies if cells should be selected if a popupmenu is displayed for
* them. Default is true.
*/
mxPopupMenuHandler.prototype.selectOnPopup = true;
/**
* Variable: clearSelectionOnBackground
*
* Specifies if cells should be deselected if a popupmenu is displayed for
* the diagram background. Default is true.
*/
mxPopupMenuHandler.prototype.clearSelectionOnBackground = true;
/**
* Variable: triggerX
*
* X-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.triggerX = null;
/**
* Variable: triggerY
*
* Y-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.triggerY = null;
/**
* Variable: screenX
*
* Screen X-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.screenX = null;
/**
* Variable: screenY
*
* Screen Y-coordinate of the mouse down event.
*/
mxPopupMenuHandler.prototype.screenY = null;
/**
* Function: init
*
* Initializes the shapes required for this vertex handler.
*/
mxPopupMenuHandler.prototype.init = function()
{
// Supercall
mxPopupMenu.prototype.init.apply(this);
// Hides the tooltip if the mouse is over
// the context menu
mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt)
{
this.graph.tooltipHandler.hide();
}));
};
/**
* Function: isSelectOnPopup
*
* Hook for returning if a cell should be selected for a given <mxMouseEvent>.
* This implementation returns <selectOnPopup>.
*/
mxPopupMenuHandler.prototype.isSelectOnPopup = function(me)
{
return this.selectOnPopup;
};
/**
* Function: mouseDown
*
* Handles the event by initiating the panning. By consuming the event all
* subsequent events of the gesture are redirected to this handler.
*/
mxPopupMenuHandler.prototype.mouseDown = function(sender, me)
{
if (this.isEnabled() && !mxEvent.isMultiTouchEvent(me.getEvent()))
{
// Hides the popupmenu if is is being displayed
this.hideMenu();
this.triggerX = me.getGraphX();
this.triggerY = me.getGraphY();
this.screenX = mxEvent.getMainEvent(me.getEvent()).screenX;
this.screenY = mxEvent.getMainEvent(me.getEvent()).screenY;
this.popupTrigger = this.isPopupTrigger(me);
this.inTolerance = true;
}
};
/**
* Function: mouseMove
*
* Handles the event by updating the panning on the graph.
*/
mxPopupMenuHandler.prototype.mouseMove = function(sender, me)
{
// Popup trigger may change on mouseUp so ignore it
if (this.inTolerance && this.screenX != null && this.screenY != null)
{
if (Math.abs(mxEvent.getMainEvent(me.getEvent()).screenX - this.screenX) > this.graph.tolerance ||
Math.abs(mxEvent.getMainEvent(me.getEvent()).screenY - this.screenY) > this.graph.tolerance)
{
this.inTolerance = false;
}
}
};
/**
* Function: mouseUp
*
* Handles the event by setting the translation on the view or showing the
* popupmenu.
*/
mxPopupMenuHandler.prototype.mouseUp = function(sender, me)
{
if (this.popupTrigger && this.inTolerance && this.triggerX != null && this.triggerY != null)
{
var cell = this.getCellForPopupEvent(me);
// Selects the cell for which the context menu is being displayed
if (this.graph.isEnabled() && this.isSelectOnPopup(me) &&
cell != null && !this.graph.isCellSelected(cell))
{
this.graph.setSelectionCell(cell);
}
else if (this.clearSelectionOnBackground && cell == null)
{
this.graph.clearSelection();
}
// Hides the tooltip if there is one
this.graph.tooltipHandler.hide();
// Menu is shifted by 1 pixel so that the mouse up event
// is routed via the underlying shape instead of the DIV
var origin = mxUtils.getScrollOrigin();
this.popup(me.getX() + origin.x + 1, me.getY() + origin.y + 1, cell, me.getEvent());
me.consume();
}
this.popupTrigger = false;
this.inTolerance = false;
};
/**
* Function: getCellForPopupEvent
*
* Hook to return the cell for the mouse up popup trigger handling.
*/
mxPopupMenuHandler.prototype.getCellForPopupEvent = function(me)
{
return me.getCell();
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPopupMenuHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
this.graph.removeListener(this.gestureHandler);
// Supercall
mxPopupMenu.prototype.destroy.apply(this);
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellMarker
*
* A helper class to process mouse locations and highlight cells.
*
* Helper class to highlight cells. To add a cell marker to an existing graph
* for highlighting all cells, the following code is used:
*
* (code)
* var marker = new mxCellMarker(graph);
* graph.addMouseListener({
* mouseDown: function() {},
* mouseMove: function(sender, me)
* {
* marker.process(me);
* },
* mouseUp: function() {}
* });
* (end)
*
* Event: mxEvent.MARK
*
* Fires after a cell has been marked or unmarked. The <code>state</code>
* property contains the marked <mxCellState> or null if no state is marked.
*
* Constructor: mxCellMarker
*
* Constructs a new cell marker.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* validColor - Optional marker color for valid states. Default is
* <mxConstants.DEFAULT_VALID_COLOR>.
* invalidColor - Optional marker color for invalid states. Default is
* <mxConstants.DEFAULT_INVALID_COLOR>.
* hotspot - Portion of the width and hight where a state intersects a
* given coordinate pair. A value of 0 means always highlight. Default is
* <mxConstants.DEFAULT_HOTSPOT>.
*/
function mxCellMarker(graph, validColor, invalidColor, hotspot)
{
mxEventSource.call(this);
if (graph != null)
{
this.graph = graph;
this.validColor = (validColor != null) ? validColor : mxConstants.DEFAULT_VALID_COLOR;
this.invalidColor = (invalidColor != null) ? invalidColor : mxConstants.DEFAULT_INVALID_COLOR;
this.hotspot = (hotspot != null) ? hotspot : mxConstants.DEFAULT_HOTSPOT;
this.highlight = new mxCellHighlight(graph);
}
};
/**
* Extends mxEventSource.
*/
mxUtils.extend(mxCellMarker, mxEventSource);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellMarker.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if the marker is enabled. Default is true.
*/
mxCellMarker.prototype.enabled = true;
/**
* Variable: hotspot
*
* Specifies the portion of the width and height that should trigger
* a highlight. The area around the center of the cell to be marked is used
* as the hotspot. Possible values are between 0 and 1. Default is
* mxConstants.DEFAULT_HOTSPOT.
*/
mxCellMarker.prototype.hotspot = mxConstants.DEFAULT_HOTSPOT;
/**
* Variable: hotspotEnabled
*
* Specifies if the hotspot is enabled. Default is false.
*/
mxCellMarker.prototype.hotspotEnabled = false;
/**
* Variable: validColor
*
* Holds the valid marker color.
*/
mxCellMarker.prototype.validColor = null;
/**
* Variable: invalidColor
*
* Holds the invalid marker color.
*/
mxCellMarker.prototype.invalidColor = null;
/**
* Variable: currentColor
*
* Holds the current marker color.
*/
mxCellMarker.prototype.currentColor = null;
/**
* Variable: validState
*
* Holds the marked <mxCellState> if it is valid.
*/
mxCellMarker.prototype.validState = null;
/**
* Variable: markedState
*
* Holds the marked <mxCellState>.
*/
mxCellMarker.prototype.markedState = null;
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxCellMarker.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxCellMarker.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setHotspot
*
* Sets the <hotspot>.
*/
mxCellMarker.prototype.setHotspot = function(hotspot)
{
this.hotspot = hotspot;
};
/**
* Function: getHotspot
*
* Returns the <hotspot>.
*/
mxCellMarker.prototype.getHotspot = function()
{
return this.hotspot;
};
/**
* Function: setHotspotEnabled
*
* Specifies whether the hotspot should be used in <intersects>.
*/
mxCellMarker.prototype.setHotspotEnabled = function(enabled)
{
this.hotspotEnabled = enabled;
};
/**
* Function: isHotspotEnabled
*
* Returns true if hotspot is used in <intersects>.
*/
mxCellMarker.prototype.isHotspotEnabled = function()
{
return this.hotspotEnabled;
};
/**
* Function: hasValidState
*
* Returns true if <validState> is not null.
*/
mxCellMarker.prototype.hasValidState = function()
{
return this.validState != null;
};
/**
* Function: getValidState
*
* Returns the <validState>.
*/
mxCellMarker.prototype.getValidState = function()
{
return this.validState;
};
/**
* Function: getMarkedState
*
* Returns the <markedState>.
*/
mxCellMarker.prototype.getMarkedState = function()
{
return this.markedState;
};
/**
* Function: reset
*
* Resets the state of the cell marker.
*/
mxCellMarker.prototype.reset = function()
{
this.validState = null;
if (this.markedState != null)
{
this.markedState = null;
this.unmark();
}
};
/**
* Function: process
*
* Processes the given event and cell and marks the state returned by
* <getState> with the color returned by <getMarkerColor>. If the
* markerColor is not null, then the state is stored in <markedState>. If
* <isValidState> returns true, then the state is stored in <validState>
* regardless of the marker color. The state is returned regardless of the
* marker color and valid state.
*/
mxCellMarker.prototype.process = function(me)
{
var state = null;
if (this.isEnabled())
{
state = this.getState(me);
this.setCurrentState(state, me);
}
return state;
};
/**
* Function: setCurrentState
*
* Sets and marks the current valid state.
*/
mxCellMarker.prototype.setCurrentState = function(state, me, color)
{
var isValid = (state != null) ? this.isValidState(state) : false;
color = (color != null) ? color : this.getMarkerColor(me.getEvent(), state, isValid);
if (isValid)
{
this.validState = state;
}
else
{
this.validState = null;
}
if (state != this.markedState || color != this.currentColor)
{
this.currentColor = color;
if (state != null && this.currentColor != null)
{
this.markedState = state;
this.mark();
}
else if (this.markedState != null)
{
this.markedState = null;
this.unmark();
}
}
};
/**
* Function: markCell
*
* Marks the given cell using the given color, or <validColor> if no color is specified.
*/
mxCellMarker.prototype.markCell = function(cell, color)
{
var state = this.graph.getView().getState(cell);
if (state != null)
{
this.currentColor = (color != null) ? color : this.validColor;
this.markedState = state;
this.mark();
}
};
/**
* Function: mark
*
* Marks the <markedState> and fires a <mark> event.
*/
mxCellMarker.prototype.mark = function()
{
this.highlight.setHighlightColor(this.currentColor);
this.highlight.highlight(this.markedState);
this.fireEvent(new mxEventObject(mxEvent.MARK, 'state', this.markedState));
};
/**
* Function: unmark
*
* Hides the marker and fires a <mark> event.
*/
mxCellMarker.prototype.unmark = function()
{
this.mark();
};
/**
* Function: isValidState
*
* Returns true if the given <mxCellState> is a valid state. If this
* returns true, then the state is stored in <validState>. The return value
* of this method is used as the argument for <getMarkerColor>.
*/
mxCellMarker.prototype.isValidState = function(state)
{
return true;
};
/**
* Function: getMarkerColor
*
* Returns the valid- or invalidColor depending on the value of isValid.
* The given <mxCellState> is ignored by this implementation.
*/
mxCellMarker.prototype.getMarkerColor = function(evt, state, isValid)
{
return (isValid) ? this.validColor : this.invalidColor;
};
/**
* Function: getState
*
* Uses <getCell>, <getStateToMark> and <intersects> to return the
* <mxCellState> for the given <mxMouseEvent>.
*/
mxCellMarker.prototype.getState = function(me)
{
var view = this.graph.getView();
var cell = this.getCell(me);
var state = this.getStateToMark(view.getState(cell));
return (state != null && this.intersects(state, me)) ? state : null;
};
/**
* Function: getCell
*
* Returns the <mxCell> for the given event and cell. This returns the
* given cell.
*/
mxCellMarker.prototype.getCell = function(me)
{
return me.getCell();
};
/**
* Function: getStateToMark
*
* Returns the <mxCellState> to be marked for the given <mxCellState> under
* the mouse. This returns the given state.
*/
mxCellMarker.prototype.getStateToMark = function(state)
{
return state;
};
/**
* Function: intersects
*
* Returns true if the given coordinate pair intersects the given state.
* This returns true if the <hotspot> is 0 or the coordinates are inside
* the hotspot for the given cell state.
*/
mxCellMarker.prototype.intersects = function(state, me)
{
if (this.hotspotEnabled)
{
return mxUtils.intersectsHotspot(state, me.getGraphX(), me.getGraphY(),
this.hotspot, mxConstants.MIN_HOTSPOT_SIZE,
mxConstants.MAX_HOTSPOT_SIZE);
}
return true;
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxCellMarker.prototype.destroy = function()
{
this.graph.getView().removeListener(this.resetHandler);
this.graph.getModel().removeListener(this.resetHandler);
this.highlight.destroy();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxSelectionCellsHandler
*
* An event handler that manages cell handlers and invokes their mouse event
* processing functions.
*
* Group: Events
*
* Event: mxEvent.ADD
*
* Fires if a cell has been added to the selection. The <code>state</code>
* property contains the <mxCellState> that has been added.
*
* Event: mxEvent.REMOVE
*
* Fires if a cell has been remove from the selection. The <code>state</code>
* property contains the <mxCellState> that has been removed.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
function mxSelectionCellsHandler(graph)
{
mxEventSource.call(this);
this.graph = graph;
this.handlers = new mxDictionary();
this.graph.addMouseListener(this);
this.refreshHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.isEnabled())
{
this.refresh();
}
});
this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.refreshHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.SCALE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.TRANSLATE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.refreshHandler);
this.graph.getView().addListener(mxEvent.DOWN, this.refreshHandler);
this.graph.getView().addListener(mxEvent.UP, this.refreshHandler);
};
/**
* Extends mxEventSource.
*/
mxUtils.extend(mxSelectionCellsHandler, mxEventSource);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxSelectionCellsHandler.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxSelectionCellsHandler.prototype.enabled = true;
/**
* Variable: refreshHandler
*
* Keeps a reference to an event listener for later removal.
*/
mxSelectionCellsHandler.prototype.refreshHandler = null;
/**
* Variable: maxHandlers
*
* Defines the maximum number of handlers to paint individually. Default is 100.
*/
mxSelectionCellsHandler.prototype.maxHandlers = 100;
/**
* Variable: handlers
*
* <mxDictionary> that maps from cells to handlers.
*/
mxSelectionCellsHandler.prototype.handlers = null;
/**
* Function: isEnabled
*
* Returns <enabled>.
*/
mxSelectionCellsHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Sets <enabled>.
*/
mxSelectionCellsHandler.prototype.setEnabled = function(value)
{
this.enabled = value;
};
/**
* Function: getHandler
*
* Returns the handler for the given cell.
*/
mxSelectionCellsHandler.prototype.getHandler = function(cell)
{
return this.handlers.get(cell);
};
/**
* Function: reset
*
* Resets all handlers.
*/
mxSelectionCellsHandler.prototype.reset = function()
{
this.handlers.visit(function(key, handler)
{
handler.reset.apply(handler);
});
};
/**
* Function: refresh
*
* Reloads or updates all handlers.
*/
mxSelectionCellsHandler.prototype.refresh = function()
{
// Removes all existing handlers
var oldHandlers = this.handlers;
this.handlers = new mxDictionary();
// Creates handles for all selection cells
var tmp = this.graph.getSelectionCells();
for (var i = 0; i < tmp.length; i++)
{
var state = this.graph.view.getState(tmp[i]);
if (state != null)
{
var handler = oldHandlers.remove(tmp[i]);
if (handler != null)
{
if (handler.state != state)
{
handler.destroy();
handler = null;
}
else if (!this.isHandlerActive(handler))
{
if (handler.refresh != null)
{
handler.refresh();
}
handler.redraw();
}
}
if (handler == null)
{
handler = this.graph.createHandler(state);
this.fireEvent(new mxEventObject(mxEvent.ADD, 'state', state));
}
if (handler != null)
{
this.handlers.put(tmp[i], handler);
}
}
}
// Destroys all unused handlers
oldHandlers.visit(mxUtils.bind(this, function(key, handler)
{
this.fireEvent(new mxEventObject(mxEvent.REMOVE, 'state', handler.state));
handler.destroy();
}));
};
/**
* Function: isHandlerActive
*
* Returns true if the given handler is active and should not be redrawn.
*/
mxSelectionCellsHandler.prototype.isHandlerActive = function(handler)
{
return handler.index != null;
};
/**
* Function: updateHandler
*
* Updates the handler for the given shape if one exists.
*/
mxSelectionCellsHandler.prototype.updateHandler = function(state)
{
var handler = this.handlers.remove(state.cell);
if (handler != null)
{
// Transfers the current state to the new handler
var index = handler.index;
var x = handler.startX;
var y = handler.startY;
handler.destroy();
handler = this.graph.createHandler(state);
if (handler != null)
{
this.handlers.put(state.cell, handler);
if (index != null && x != null && y != null)
{
handler.start(x, y, index);
}
}
}
};
/**
* Function: mouseDown
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseDown = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseDown.apply(handler, args);
});
}
};
/**
* Function: mouseMove
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseMove = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseMove.apply(handler, args);
});
}
};
/**
* Function: mouseUp
*
* Redirects the given event to the handlers.
*/
mxSelectionCellsHandler.prototype.mouseUp = function(sender, me)
{
if (this.graph.isEnabled() && this.isEnabled())
{
var args = [sender, me];
this.handlers.visit(function(key, handler)
{
handler.mouseUp.apply(handler, args);
});
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxSelectionCellsHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
if (this.refreshHandler != null)
{
this.graph.getSelectionModel().removeListener(this.refreshHandler);
this.graph.getModel().removeListener(this.refreshHandler);
this.graph.getView().removeListener(this.refreshHandler);
this.refreshHandler = null;
}
};
/**
* Copyright (c) 2006-2016, JGraph Ltd
* Copyright (c) 2006-2016, Gaudenz Alder
*/
/**
* Class: mxConnectionHandler
*
* Graph event handler that creates new connections. Uses <mxTerminalMarker>
* for finding and highlighting the source and target vertices and
* <factoryMethod> to create the edge instance. This handler is built-into
* <mxGraph.connectionHandler> and enabled using <mxGraph.setConnectable>.
*
* Example:
*
* (code)
* new mxConnectionHandler(graph, function(source, target, style)
* {
* edge = new mxCell('', new mxGeometry());
* edge.setEdge(true);
* edge.setStyle(style);
* edge.geometry.relative = true;
* return edge;
* });
* (end)
*
* Here is an alternative solution that just sets a specific user object for
* new edges by overriding <insertEdge>.
*
* (code)
* mxConnectionHandlerInsertEdge = mxConnectionHandler.prototype.insertEdge;
* mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style)
* {
* value = 'Test';
*
* return mxConnectionHandlerInsertEdge.apply(this, arguments);
* };
* (end)
*
* Using images to trigger connections:
*
* This handler uses mxTerminalMarker to find the source and target cell for
* the new connection and creates a new edge using <connect>. The new edge is
* created using <createEdge> which in turn uses <factoryMethod> or creates a
* new default edge.
*
* The handler uses a "highlight-paradigm" for indicating if a cell is being
* used as a source or target terminal, as seen in other diagramming products.
* In order to allow both, moving and connecting cells at the same time,
* <mxConstants.DEFAULT_HOTSPOT> is used in the handler to determine the hotspot
* of a cell, that is, the region of the cell which is used to trigger a new
* connection. The constant is a value between 0 and 1 that specifies the
* amount of the width and height around the center to be used for the hotspot
* of a cell and its default value is 0.5. In addition,
* <mxConstants.MIN_HOTSPOT_SIZE> defines the minimum number of pixels for the
* width and height of the hotspot.
*
* This solution, while standards compliant, may be somewhat confusing because
* there is no visual indicator for the hotspot and the highlight is seen to
* switch on and off while the mouse is being moved in and out. Furthermore,
* this paradigm does not allow to create different connections depending on
* the highlighted hotspot as there is only one hotspot per cell and it
* normally does not allow cells to be moved and connected at the same time as
* there is no clear indication of the connectable area of the cell.
*
* To come across these issues, the handle has an additional <createIcons> hook
* with a default implementation that allows to create one icon to be used to
* trigger new connections. If this icon is specified, then new connections can
* only be created if the image is clicked while the cell is being highlighted.
* The <createIcons> hook may be overridden to create more than one
* <mxImageShape> for creating new connections, but the default implementation
* supports one image and is used as follows:
*
* In order to display the "connect image" whenever the mouse is over the cell,
* an DEFAULT_HOTSPOT of 1 should be used:
*
* (code)
* mxConstants.DEFAULT_HOTSPOT = 1;
* (end)
*
* In order to avoid confusion with the highlighting, the highlight color
* should not be used with a connect image:
*
* (code)
* mxConstants.HIGHLIGHT_COLOR = null;
* (end)
*
* To install the image, the connectImage field of the mxConnectionHandler must
* be assigned a new <mxImage> instance:
*
* (code)
* mxConnectionHandler.prototype.connectImage = new mxImage('images/green-dot.gif', 14, 14);
* (end)
*
* This will use the green-dot.gif with a width and height of 14 pixels as the
* image to trigger new connections. In createIcons the icon field of the
* handler will be set in order to remember the icon that has been clicked for
* creating the new connection. This field will be available under selectedIcon
* in the connect method, which may be overridden to take the icon that
* triggered the new connection into account. This is useful if more than one
* icon may be used to create a connection.
*
* Group: Events
*
* Event: mxEvent.START
*
* Fires when a new connection is being created by the user. The <code>state</code>
* property contains the state of the source cell.
*
* Event: mxEvent.CONNECT
*
* Fires between begin- and endUpdate in <connect>. The <code>cell</code>
* property contains the inserted edge, the <code>event</code> and <code>target</code>
* properties contain the respective arguments that were passed to <connect> (where
* target corresponds to the dropTarget argument). Finally, the <code>terminal</code>
* property corresponds to the target argument in <connect> or the clone of the source
* terminal if <createTarget> is enabled.
*
* Note that the target is the cell under the mouse where the mouse button was released.
* Depending on the logic in the handler, this doesn't necessarily have to be the target
* of the inserted edge. To print the source, target or any optional ports IDs that the
* edge is connected to, the following code can be used. To get more details about the
* actual connection point, <mxGraph.getConnectionConstraint> can be used. To resolve
* the port IDs, use <mxGraphModel.getCell>.
*
* (code)
* graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt)
* {
* var edge = evt.getProperty('cell');
* var source = graph.getModel().getTerminal(edge, true);
* var target = graph.getModel().getTerminal(edge, false);
*
* var style = graph.getCellStyle(edge);
* var sourcePortId = style[mxConstants.STYLE_SOURCE_PORT];
* var targetPortId = style[mxConstants.STYLE_TARGET_PORT];
*
* mxLog.show();
* mxLog.debug('connect', edge, source.id, target.id, sourcePortId, targetPortId);
* });
* (end)
*
* Event: mxEvent.RESET
*
* Fires when the <reset> method is invoked.
*
* Constructor: mxConnectionHandler
*
* Constructs an event handler that connects vertices using the specified
* factory method to create the new edges. Modify
* <mxConstants.ACTIVE_REGION> to setup the region on a cell which triggers
* the creation of a new connection or use connect icons as explained
* above.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* factoryMethod - Optional function to create the edge. The function takes
* the source and target <mxCell> as the first and second argument and an
* optional cell style from the preview as the third argument. It returns
* the <mxCell> that represents the new edge.
*/
function mxConnectionHandler(graph, factoryMethod)
{
mxEventSource.call(this);
if (graph != null)
{
this.graph = graph;
this.factoryMethod = factoryMethod;
this.init();
// Handles escape keystrokes
this.escapeHandler = mxUtils.bind(this, function(sender, evt)
{
this.reset();
});
this.graph.addListener(mxEvent.ESCAPE, this.escapeHandler);
}
};
/**
* Extends mxEventSource.
*/
mxUtils.extend(mxConnectionHandler, mxEventSource);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxConnectionHandler.prototype.graph = null;
/**
* Variable: factoryMethod
*
* Function that is used for creating new edges. The function takes the
* source and target <mxCell> as the first and second argument and returns
* a new <mxCell> that represents the edge. This is used in <createEdge>.
*/
mxConnectionHandler.prototype.factoryMethod = true;
/**
* Variable: moveIconFront
*
* Specifies if icons should be displayed inside the graph container instead
* of the overlay pane. This is used for HTML labels on vertices which hide
* the connect icon. This has precendence over <moveIconBack> when set
* to true. Default is false.
*/
mxConnectionHandler.prototype.moveIconFront = false;
/**
* Variable: moveIconBack
*
* Specifies if icons should be moved to the back of the overlay pane. This can
* be set to true if the icons of the connection handler conflict with other
* handles, such as the vertex label move handle. Default is false.
*/
mxConnectionHandler.prototype.moveIconBack = false;
/**
* Variable: connectImage
*
* <mxImage> that is used to trigger the creation of a new connection. This
* is used in <createIcons>. Default is null.
*/
mxConnectionHandler.prototype.connectImage = null;
/**
* Variable: targetConnectImage
*
* Specifies if the connect icon should be centered on the target state
* while connections are being previewed. Default is false.
*/
mxConnectionHandler.prototype.targetConnectImage = false;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxConnectionHandler.prototype.enabled = true;
/**
* Variable: select
*
* Specifies if new edges should be selected. Default is true.
*/
mxConnectionHandler.prototype.select = true;
/**
* Variable: createTarget
*
* Specifies if <createTargetVertex> should be called if no target was under the
* mouse for the new connection. Setting this to true means the connection
* will be drawn as valid if no target is under the mouse, and
* <createTargetVertex> will be called before the connection is created between
* the source cell and the newly created vertex in <createTargetVertex>, which
* can be overridden to create a new target. Default is false.
*/
mxConnectionHandler.prototype.createTarget = false;
/**
* Variable: marker
*
* Holds the <mxTerminalMarker> used for finding source and target cells.
*/
mxConnectionHandler.prototype.marker = null;
/**
* Variable: constraintHandler
*
* Holds the <mxConstraintHandler> used for drawing and highlighting
* constraints.
*/
mxConnectionHandler.prototype.constraintHandler = null;
/**
* Variable: error
*
* Holds the current validation error while connections are being created.
*/
mxConnectionHandler.prototype.error = null;
/**
* Variable: waypointsEnabled
*
* Specifies if single clicks should add waypoints on the new edge. Default is
* false.
*/
mxConnectionHandler.prototype.waypointsEnabled = false;
/**
* Variable: ignoreMouseDown
*
* Specifies if the connection handler should ignore the state of the mouse
* button when highlighting the source. Default is false, that is, the
* handler only highlights the source if no button is being pressed.
*/
mxConnectionHandler.prototype.ignoreMouseDown = false;
/**
* Variable: first
*
* Holds the <mxPoint> where the mouseDown took place while the handler is
* active.
*/
mxConnectionHandler.prototype.first = null;
/**
* Variable: connectIconOffset
*
* Holds the offset for connect icons during connection preview.
* Default is mxPoint(0, <mxConstants.TOOLTIP_VERTICAL_OFFSET>).
* Note that placing the icon under the mouse pointer with an
* offset of (0,0) will affect hit detection.
*/
mxConnectionHandler.prototype.connectIconOffset = new mxPoint(0, mxConstants.TOOLTIP_VERTICAL_OFFSET);
/**
* Variable: edgeState
*
* Optional <mxCellState> that represents the preview edge while the
* handler is active. This is created in <createEdgeState>.
*/
mxConnectionHandler.prototype.edgeState = null;
/**
* Variable: changeHandler
*
* Holds the change event listener for later removal.
*/
mxConnectionHandler.prototype.changeHandler = null;
/**
* Variable: drillHandler
*
* Holds the drill event listener for later removal.
*/
mxConnectionHandler.prototype.drillHandler = null;
/**
* Variable: mouseDownCounter
*
* Counts the number of mouseDown events since the start. The initial mouse
* down event counts as 1.
*/
mxConnectionHandler.prototype.mouseDownCounter = 0;
/**
* Variable: movePreviewAway
*
* Switch to enable moving the preview away from the mousepointer. This is required in browsers
* where the preview cannot be made transparent to events and if the built-in hit detection on
* the HTML elements in the page should be used. Default is the value of <mxClient.IS_VML>.
*/
mxConnectionHandler.prototype.movePreviewAway = mxClient.IS_VML;
/**
* Variable: outlineConnect
*
* Specifies if connections to the outline of a highlighted target should be
* enabled. This will allow to place the connection point along the outline of
* the highlighted target. Default is false.
*/
mxConnectionHandler.prototype.outlineConnect = false;
/**
* Variable: livePreview
*
* Specifies if the actual shape of the edge state should be used for the preview.
* Default is false. (Ignored if no edge state is created in <createEdgeState>.)
*/
mxConnectionHandler.prototype.livePreview = false;
/**
* Variable: cursor
*
* Specifies the cursor to be used while the handler is active. Default is null.
*/
mxConnectionHandler.prototype.cursor = null;
/**
* Variable: insertBeforeSource
*
* Specifies if new edges should be inserted before the source vertex in the
* cell hierarchy. Default is false for backwards compatibility.
*/
mxConnectionHandler.prototype.insertBeforeSource = false;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxConnectionHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxConnectionHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isInsertBefore
*
* Returns <insertBeforeSource> for non-loops and false for loops.
*
* Parameters:
*
* edge - <mxCell> that represents the edge to be inserted.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
* evt - Mousedown event of the connect gesture.
* dropTarget - <mxCell> that represents the cell under the mouse when it was
* released.
*/
mxConnectionHandler.prototype.isInsertBefore = function(edge, source, target, evt, dropTarget)
{
return this.insertBeforeSource && source != target;
};
/**
* Function: isCreateTarget
*
* Returns <createTarget>.
*
* Parameters:
*
* evt - Current active native pointer event.
*/
mxConnectionHandler.prototype.isCreateTarget = function(evt)
{
return this.createTarget;
};
/**
* Function: setCreateTarget
*
* Sets <createTarget>.
*/
mxConnectionHandler.prototype.setCreateTarget = function(value)
{
this.createTarget = value;
};
/**
* Function: createShape
*
* Creates the preview shape for new connections.
*/
mxConnectionHandler.prototype.createShape = function()
{
// Creates the edge preview
var shape = (this.livePreview && this.edgeState != null) ?
this.graph.cellRenderer.createShape(this.edgeState) :
new mxPolyline([], mxConstants.INVALID_COLOR);
shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
shape.scale = this.graph.view.scale;
shape.pointerEvents = false;
shape.isDashed = true;
shape.init(this.graph.getView().getOverlayPane());
mxEvent.redirectMouseEvents(shape.node, this.graph, null);
return shape;
};
/**
* Function: init
*
* Initializes the shapes required for this connection handler. This should
* be invoked if <mxGraph.container> is assigned after the connection
* handler has been created.
*/
mxConnectionHandler.prototype.init = function()
{
this.graph.addMouseListener(this);
this.marker = this.createMarker();
this.constraintHandler = new mxConstraintHandler(this.graph);
// Redraws the icons if the graph changes
this.changeHandler = mxUtils.bind(this, function(sender)
{
if (this.iconState != null)
{
this.iconState = this.graph.getView().getState(this.iconState.cell);
}
if (this.iconState != null)
{
this.redrawIcons(this.icons, this.iconState);
this.constraintHandler.reset();
}
else if (this.previous != null && this.graph.view.getState(this.previous.cell) == null)
{
this.reset();
}
});
this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
this.graph.getView().addListener(mxEvent.SCALE, this.changeHandler);
this.graph.getView().addListener(mxEvent.TRANSLATE, this.changeHandler);
this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.changeHandler);
// Removes the icon if we step into/up or start editing
this.drillHandler = mxUtils.bind(this, function(sender)
{
this.reset();
});
this.graph.addListener(mxEvent.START_EDITING, this.drillHandler);
this.graph.getView().addListener(mxEvent.DOWN, this.drillHandler);
this.graph.getView().addListener(mxEvent.UP, this.drillHandler);
};
/**
* Function: isConnectableCell
*
* Returns true if the given cell is connectable. This is a hook to
* disable floating connections. This implementation returns true.
*/
mxConnectionHandler.prototype.isConnectableCell = function(cell)
{
return true;
};
/**
* Function: createMarker
*
* Creates and returns the <mxCellMarker> used in <marker>.
*/
mxConnectionHandler.prototype.createMarker = function()
{
var marker = new mxCellMarker(this.graph);
marker.hotspotEnabled = true;
// Overrides to return cell at location only if valid (so that
// there is no highlight for invalid cells)
marker.getCell = mxUtils.bind(this, function(me)
{
var cell = mxCellMarker.prototype.getCell.apply(marker, arguments);
this.error = null;
// Checks for cell at preview point (with grid)
if (cell == null && this.currentPoint != null)
{
cell = this.graph.getCellAt(this.currentPoint.x, this.currentPoint.y);
}
// Uses connectable parent vertex if one exists
if (cell != null && !this.graph.isCellConnectable(cell))
{
var parent = this.graph.getModel().getParent(cell);
if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
{
cell = parent;
}
}
if ((this.graph.isSwimlane(cell) && this.currentPoint != null &&
this.graph.hitsSwimlaneContent(cell, this.currentPoint.x, this.currentPoint.y)) ||
!this.isConnectableCell(cell))
{
cell = null;
}
if (cell != null)
{
if (this.isConnecting())
{
if (this.previous != null)
{
this.error = this.validateConnection(this.previous.cell, cell);
if (this.error != null && this.error.length == 0)
{
cell = null;
// Enables create target inside groups
if (this.isCreateTarget(me.getEvent()))
{
this.error = null;
}
}
}
}
else if (!this.isValidSource(cell, me))
{
cell = null;
}
}
else if (this.isConnecting() && !this.isCreateTarget(me.getEvent()) &&
!this.graph.allowDanglingEdges)
{
this.error = '';
}
return cell;
});
// Sets the highlight color according to validateConnection
marker.isValidState = mxUtils.bind(this, function(state)
{
if (this.isConnecting())
{
return this.error == null;
}
else
{
return mxCellMarker.prototype.isValidState.apply(marker, arguments);
}
});
// Overrides to use marker color only in highlight mode or for
// target selection
marker.getMarkerColor = mxUtils.bind(this, function(evt, state, isValid)
{
return (this.connectImage == null || this.isConnecting()) ?
mxCellMarker.prototype.getMarkerColor.apply(marker, arguments) :
null;
});
// Overrides to use hotspot only for source selection otherwise
// intersects always returns true when over a cell
marker.intersects = mxUtils.bind(this, function(state, evt)
{
if (this.connectImage != null || this.isConnecting())
{
return true;
}
return mxCellMarker.prototype.intersects.apply(marker, arguments);
});
return marker;
};
/**
* Function: start
*
* Starts a new connection for the given state and coordinates.
*/
mxConnectionHandler.prototype.start = function(state, x, y, edgeState)
{
this.previous = state;
this.first = new mxPoint(x, y);
this.edgeState = (edgeState != null) ? edgeState : this.createEdgeState(null);
// Marks the source state
this.marker.currentColor = this.marker.validColor;
this.marker.markedState = state;
this.marker.mark();
this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous));
};
/**
* Function: isConnecting
*
* Returns true if the source terminal has been clicked and a new
* connection is currently being previewed.
*/
mxConnectionHandler.prototype.isConnecting = function()
{
return this.first != null && this.shape != null;
};
/**
* Function: isValidSource
*
* Returns <mxGraph.isValidSource> for the given source terminal.
*
* Parameters:
*
* cell - <mxCell> that represents the source terminal.
* me - <mxMouseEvent> that is associated with this call.
*/
mxConnectionHandler.prototype.isValidSource = function(cell, me)
{
return this.graph.isValidSource(cell);
};
/**
* Function: isValidTarget
*
* Returns true. The call to <mxGraph.isValidTarget> is implicit by calling
* <mxGraph.getEdgeValidationError> in <validateConnection>. This is an
* additional hook for disabling certain targets in this specific handler.
*
* Parameters:
*
* cell - <mxCell> that represents the target terminal.
*/
mxConnectionHandler.prototype.isValidTarget = function(cell)
{
return true;
};
/**
* Function: validateConnection
*
* Returns the error message or an empty string if the connection for the
* given source target pair is not valid. Otherwise it returns null. This
* implementation uses <mxGraph.getEdgeValidationError>.
*
* Parameters:
*
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
*/
mxConnectionHandler.prototype.validateConnection = function(source, target)
{
if (!this.isValidTarget(target))
{
return '';
}
return this.graph.getEdgeValidationError(null, source, target);
};
/**
* Function: getConnectImage
*
* Hook to return the <mxImage> used for the connection icon of the given
* <mxCellState>. This implementation returns <connectImage>.
*
* Parameters:
*
* state - <mxCellState> whose connect image should be returned.
*/
mxConnectionHandler.prototype.getConnectImage = function(state)
{
return this.connectImage;
};
/**
* Function: isMoveIconToFrontForState
*
* Returns true if the state has a HTML label in the graph's container, otherwise
* it returns <moveIconFront>.
*
* Parameters:
*
* state - <mxCellState> whose connect icons should be returned.
*/
mxConnectionHandler.prototype.isMoveIconToFrontForState = function(state)
{
if (state.text != null && state.text.node.parentNode == this.graph.container)
{
return true;
}
return this.moveIconFront;
};
/**
* Function: createIcons
*
* Creates the array <mxImageShapes> that represent the connect icons for
* the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> whose connect icons should be returned.
*/
mxConnectionHandler.prototype.createIcons = function(state)
{
var image = this.getConnectImage(state);
if (image != null && state != null)
{
this.iconState = state;
var icons = [];
// Cannot use HTML for the connect icons because the icon receives all
// mouse move events in IE, must use VML and SVG instead even if the
// connect-icon appears behind the selection border and the selection
// border consumes the events before the icon gets a chance
var bounds = new mxRectangle(0, 0, image.width, image.height);
var icon = new mxImageShape(bounds, image.src, null, null, 0);
icon.preserveImageAspect = false;
if (this.isMoveIconToFrontForState(state))
{
icon.dialect = mxConstants.DIALECT_STRICTHTML;
icon.init(this.graph.container);
}
else
{
icon.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML;
icon.init(this.graph.getView().getOverlayPane());
// Move the icon back in the overlay pane
if (this.moveIconBack && icon.node.previousSibling != null)
{
icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
}
}
icon.node.style.cursor = mxConstants.CURSOR_CONNECT;
// Events transparency
var getState = mxUtils.bind(this, function()
{
return (this.currentState != null) ? this.currentState : state;
});
// Updates the local icon before firing the mouse down event.
var mouseDown = mxUtils.bind(this, function(evt)
{
if (!mxEvent.isConsumed(evt))
{
this.icon = icon;
this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN,
new mxMouseEvent(evt, getState()));
}
});
mxEvent.redirectMouseEvents(icon.node, this.graph, getState, mouseDown);
icons.push(icon);
this.redrawIcons(icons, this.iconState);
return icons;
}
return null;
};
/**
* Function: redrawIcons
*
* Redraws the given array of <mxImageShapes>.
*
* Parameters:
*
* icons - Optional array of <mxImageShapes> to be redrawn.
*/
mxConnectionHandler.prototype.redrawIcons = function(icons, state)
{
if (icons != null && icons[0] != null && state != null)
{
var pos = this.getIconPosition(icons[0], state);
icons[0].bounds.x = pos.x;
icons[0].bounds.y = pos.y;
icons[0].redraw();
}
};
/**
* Function: redrawIcons
*
* Redraws the given array of <mxImageShapes>.
*
* Parameters:
*
* icons - Optional array of <mxImageShapes> to be redrawn.
*/
mxConnectionHandler.prototype.getIconPosition = function(icon, state)
{
var scale = this.graph.getView().scale;
var cx = state.getCenterX();
var cy = state.getCenterY();
if (this.graph.isSwimlane(state.cell))
{
var size = this.graph.getStartSize(state.cell);
cx = (size.width != 0) ? state.x + size.width * scale / 2 : cx;
cy = (size.height != 0) ? state.y + size.height * scale / 2 : cy;
var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0);
if (alpha != 0)
{
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var ct = new mxPoint(state.getCenterX(), state.getCenterY());
var pt = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin, ct);
cx = pt.x;
cy = pt.y;
}
}
return new mxPoint(cx - icon.bounds.width / 2,
cy - icon.bounds.height / 2);
};
/**
* Function: destroyIcons
*
* Destroys the connect icons and resets the respective state.
*/
mxConnectionHandler.prototype.destroyIcons = function()
{
if (this.icons != null)
{
for (var i = 0; i < this.icons.length; i++)
{
this.icons[i].destroy();
}
this.icons = null;
this.icon = null;
this.selectedIcon = null;
this.iconState = null;
}
};
/**
* Function: isStartEvent
*
* Returns true if the given mouse down event should start this handler. The
* This implementation returns true if the event does not force marquee
* selection, and the currentConstraint and currentFocus of the
* <constraintHandler> are not null, or <previous> and <error> are not null and
* <icons> is null or <icons> and <icon> are not null.
*/
mxConnectionHandler.prototype.isStartEvent = function(me)
{
return ((this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null) ||
(this.previous != null && this.error == null && (this.icons == null || (this.icons != null &&
this.icon != null))));
};
/**
* Function: mouseDown
*
* Handles the event by initiating a new connection.
*/
mxConnectionHandler.prototype.mouseDown = function(sender, me)
{
this.mouseDownCounter++;
if (this.isEnabled() && this.graph.isEnabled() && !me.isConsumed() &&
!this.isConnecting() && this.isStartEvent(me))
{
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null &&
this.constraintHandler.currentPoint != null)
{
this.sourceConstraint = this.constraintHandler.currentConstraint;
this.previous = this.constraintHandler.currentFocus;
this.first = this.constraintHandler.currentPoint.clone();
}
else
{
// Stores the location of the initial mousedown
this.first = new mxPoint(me.getGraphX(), me.getGraphY());
}
this.edgeState = this.createEdgeState(me);
this.mouseDownCounter = 1;
if (this.waypointsEnabled && this.shape == null)
{
this.waypoints = null;
this.shape = this.createShape();
if (this.edgeState != null)
{
this.shape.apply(this.edgeState);
}
}
// Stores the starting point in the geometry of the preview
if (this.previous == null && this.edgeState != null)
{
var pt = this.graph.getPointForEvent(me.getEvent());
this.edgeState.cell.geometry.setTerminalPoint(pt, true);
}
this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous));
me.consume();
}
this.selectedIcon = this.icon;
this.icon = null;
};
/**
* Function: isImmediateConnectSource
*
* Returns true if a tap on the given source state should immediately start
* connecting. This implementation returns true if the state is not movable
* in the graph.
*/
mxConnectionHandler.prototype.isImmediateConnectSource = function(state)
{
return !this.graph.isCellMovable(state.cell);
};
/**
* Function: createEdgeState
*
* Hook to return an <mxCellState> which may be used during the preview.
* This implementation returns null.
*
* Use the following code to create a preview for an existing edge style:
*
* (code)
* graph.connectionHandler.createEdgeState = function(me)
* {
* var edge = graph.createEdge(null, null, null, null, null, 'edgeStyle=elbowEdgeStyle');
*
* return new mxCellState(this.graph.view, edge, this.graph.getCellStyle(edge));
* };
* (end)
*/
mxConnectionHandler.prototype.createEdgeState = function(me)
{
return null;
};
/**
* Function: isOutlineConnectEvent
*
* Returns true if <outlineConnect> is true and the source of the event is the outline shape
* or shift is pressed.
*/
mxConnectionHandler.prototype.isOutlineConnectEvent = function(me)
{
var offset = mxUtils.getOffset(this.graph.container);
var evt = me.getEvent();
var clientX = mxEvent.getClientX(evt);
var clientY = mxEvent.getClientY(evt);
var doc = document.documentElement;
var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left;
var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top;
return this.outlineConnect && !mxEvent.isShiftDown(me.getEvent()) &&
(me.isSource(this.marker.highlight.shape) ||
(mxEvent.isAltDown(me.getEvent()) && me.getState() != null) ||
this.marker.highlight.isHighlightAt(clientX, clientY) ||
((gridX != clientX || gridY != clientY) && me.getState() == null &&
this.marker.highlight.isHighlightAt(gridX, gridY)));
};
/**
* Function: updateCurrentState
*
* Updates the current state for a given mouse move event by using
* the <marker>.
*/
mxConnectionHandler.prototype.updateCurrentState = function(me, point)
{
this.constraintHandler.update(me, this.first == null, false, (this.first == null ||
me.isSource(this.marker.highlight.shape)) ? null : point);
if (this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null)
{
// Handles special case where grid is large and connection point is at actual point in which
// case the outline is not followed as long as we're < gridSize / 2 away from that point
if (this.marker.highlight != null && this.marker.highlight.state != null &&
this.marker.highlight.state.cell == this.constraintHandler.currentFocus.cell)
{
// Direct repaint needed if cell already highlighted
if (this.marker.highlight.shape.stroke != 'transparent')
{
this.marker.highlight.shape.stroke = 'transparent';
this.marker.highlight.repaint();
}
}
else
{
this.marker.markCell(this.constraintHandler.currentFocus.cell, 'transparent');
}
// Updates validation state
if (this.previous != null)
{
this.error = this.validateConnection(this.previous.cell, this.constraintHandler.currentFocus.cell);
if (this.error == null)
{
this.currentState = this.constraintHandler.currentFocus;
}
else
{
this.constraintHandler.reset();
}
}
}
else
{
if (this.graph.isIgnoreTerminalEvent(me.getEvent()))
{
this.marker.reset();
this.currentState = null;
}
else
{
this.marker.process(me);
this.currentState = this.marker.getValidState();
if (this.currentState != null && !this.isCellEnabled(this.currentState.cell))
{
this.currentState = null;
}
}
var outline = this.isOutlineConnectEvent(me);
if (this.currentState != null && outline)
{
// Handles special case where mouse is on outline away from actual end point
// in which case the grid is ignored and mouse point is used instead
if (me.isSource(this.marker.highlight.shape))
{
point = new mxPoint(me.getGraphX(), me.getGraphY());
}
var constraint = this.graph.getOutlineConstraint(point, this.currentState, me);
this.constraintHandler.setFocus(me, this.currentState, false);
this.constraintHandler.currentConstraint = constraint;
this.constraintHandler.currentPoint = point;
}
if (this.outlineConnect)
{
if (this.marker.highlight != null && this.marker.highlight.shape != null)
{
var s = this.graph.view.scale;
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null)
{
this.marker.highlight.shape.stroke = mxConstants.OUTLINE_HIGHLIGHT_COLOR;
this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s;
this.marker.highlight.repaint();
}
else if (this.marker.hasValidState())
{
// Handles special case where actual end point of edge and current mouse point
// are not equal (due to grid snapping) and there is no hit on shape or highlight
if (this.marker.getValidState() != me.getState())
{
this.marker.highlight.shape.stroke = 'transparent';
this.currentState = null;
}
else
{
this.marker.highlight.shape.stroke = mxConstants.DEFAULT_VALID_COLOR;
}
this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s;
this.marker.highlight.repaint();
}
}
}
}
};
/**
* Function: isCellEnabled
*
* Returns true if the given cell does not allow new connections to be created.
*/
mxConnectionHandler.prototype.isCellEnabled = function(cell)
{
return true;
};
/**
* Function: convertWaypoint
*
* Converts the given point from screen coordinates to model coordinates.
*/
mxConnectionHandler.prototype.convertWaypoint = function(point)
{
var scale = this.graph.getView().getScale();
var tr = this.graph.getView().getTranslate();
point.x = point.x / scale - tr.x;
point.y = point.y / scale - tr.y;
};
/**
* Function: snapToPreview
*
* Called to snap the given point to the current preview. This snaps to the
* first point of the preview if alt is not pressed.
*/
mxConnectionHandler.prototype.snapToPreview = function(me, point)
{
if (!mxEvent.isAltDown(me.getEvent()) && this.previous != null)
{
var tol = this.graph.gridSize * this.graph.view.scale / 2;
var tmp = (this.sourceConstraint != null) ? this.first :
new mxPoint(this.previous.getCenterX(), this.previous.getCenterY());
if (Math.abs(tmp.x - me.getGraphX()) < tol)
{
point.x = tmp.x;
}
if (Math.abs(tmp.y - me.getGraphY()) < tol)
{
point.y = tmp.y;
}
}
};
/**
* Function: mouseMove
*
* Handles the event by updating the preview edge or by highlighting
* a possible source or target terminal.
*/
mxConnectionHandler.prototype.mouseMove = function(sender, me)
{
if (!me.isConsumed() && (this.ignoreMouseDown || this.first != null || !this.graph.isMouseDown))
{
// Handles special case when handler is disabled during highlight
if (!this.isEnabled() && this.currentState != null)
{
this.destroyIcons();
this.currentState = null;
}
var view = this.graph.getView();
var scale = view.scale;
var tr = view.translate;
var point = new mxPoint(me.getGraphX(), me.getGraphY());
this.error = null;
if (this.graph.isGridEnabledEvent(me.getEvent()))
{
point = new mxPoint((this.graph.snap(point.x / scale - tr.x) + tr.x) * scale,
(this.graph.snap(point.y / scale - tr.y) + tr.y) * scale);
}
this.snapToPreview(me, point);
this.currentPoint = point;
if ((this.first != null || (this.isEnabled() && this.graph.isEnabled())) &&
(this.shape != null || this.first == null ||
Math.abs(me.getGraphX() - this.first.x) > this.graph.tolerance ||
Math.abs(me.getGraphY() - this.first.y) > this.graph.tolerance))
{
this.updateCurrentState(me, point);
}
if (this.first != null)
{
var constraint = null;
var current = point;
// Uses the current point from the constraint handler if available
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null &&
this.constraintHandler.currentPoint != null)
{
constraint = this.constraintHandler.currentConstraint;
current = this.constraintHandler.currentPoint.clone();
}
else if (this.previous != null && !this.graph.isIgnoreTerminalEvent(me.getEvent()) &&
mxEvent.isShiftDown(me.getEvent()))
{
if (Math.abs(this.previous.getCenterX() - point.x) <
Math.abs(this.previous.getCenterY() - point.y))
{
point.x = this.previous.getCenterX();
}
else
{
point.y = this.previous.getCenterY();
}
}
var pt2 = this.first;
// Moves the connect icon with the mouse
if (this.selectedIcon != null)
{
var w = this.selectedIcon.bounds.width;
var h = this.selectedIcon.bounds.height;
if (this.currentState != null && this.targetConnectImage)
{
var pos = this.getIconPosition(this.selectedIcon, this.currentState);
this.selectedIcon.bounds.x = pos.x;
this.selectedIcon.bounds.y = pos.y;
}
else
{
var bounds = new mxRectangle(me.getGraphX() + this.connectIconOffset.x,
me.getGraphY() + this.connectIconOffset.y, w, h);
this.selectedIcon.bounds = bounds;
}
this.selectedIcon.redraw();
}
// Uses edge state to compute the terminal points
if (this.edgeState != null)
{
this.updateEdgeState(current, constraint);
current = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 1];
pt2 = this.edgeState.absolutePoints[0];
}
else
{
if (this.currentState != null)
{
if (this.constraintHandler.currentConstraint == null)
{
var tmp = this.getTargetPerimeterPoint(this.currentState, me);
if (tmp != null)
{
current = tmp;
}
}
}
// Computes the source perimeter point
if (this.sourceConstraint == null && this.previous != null)
{
var next = (this.waypoints != null && this.waypoints.length > 0) ?
this.waypoints[0] : current;
var tmp = this.getSourcePerimeterPoint(this.previous, next, me);
if (tmp != null)
{
pt2 = tmp;
}
}
}
// Makes sure the cell under the mousepointer can be detected
// by moving the preview shape away from the mouse. This
// makes sure the preview shape does not prevent the detection
// of the cell under the mousepointer even for slow gestures.
if (this.currentState == null && this.movePreviewAway)
{
var tmp = pt2;
if (this.edgeState != null && this.edgeState.absolutePoints.length >= 2)
{
var tmp2 = this.edgeState.absolutePoints[this.edgeState.absolutePoints.length - 2];
if (tmp2 != null)
{
tmp = tmp2;
}
}
var dx = current.x - tmp.x;
var dy = current.y - tmp.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len == 0)
{
return;
}
// Stores old point to reuse when creating edge
this.originalPoint = current.clone();
current.x -= dx * 4 / len;
current.y -= dy * 4 / len;
}
else
{
this.originalPoint = null;
}
// Creates the preview shape (lazy)
if (this.shape == null)
{
var dx = Math.abs(me.getGraphX() - this.first.x);
var dy = Math.abs(me.getGraphY() - this.first.y);
if (dx > this.graph.tolerance || dy > this.graph.tolerance)
{
this.shape = this.createShape();
if (this.edgeState != null)
{
this.shape.apply(this.edgeState);
}
// Revalidates current connection
this.updateCurrentState(me, point);
}
}
// Updates the points in the preview edge
if (this.shape != null)
{
if (this.edgeState != null)
{
this.shape.points = this.edgeState.absolutePoints;
}
else
{
var pts = [pt2];
if (this.waypoints != null)
{
pts = pts.concat(this.waypoints);
}
pts.push(current);
this.shape.points = pts;
}
this.drawPreview();
}
// Makes sure endpoint of edge is visible during connect
if (this.cursor != null)
{
this.graph.container.style.cursor = this.cursor;
}
mxEvent.consume(me.getEvent());
me.consume();
}
else if (!this.isEnabled() || !this.graph.isEnabled())
{
this.constraintHandler.reset();
}
else if (this.previous != this.currentState && this.edgeState == null)
{
this.destroyIcons();
// Sets the cursor on the current shape
if (this.currentState != null && this.error == null && this.constraintHandler.currentConstraint == null)
{
this.icons = this.createIcons(this.currentState);
if (this.icons == null)
{
this.currentState.setCursor(mxConstants.CURSOR_CONNECT);
me.consume();
}
}
this.previous = this.currentState;
}
else if (this.previous == this.currentState && this.currentState != null && this.icons == null &&
!this.graph.isMouseDown)
{
// Makes sure that no cursors are changed
me.consume();
}
if (!this.graph.isMouseDown && this.currentState != null && this.icons != null)
{
var hitsIcon = false;
var target = me.getSource();
for (var i = 0; i < this.icons.length && !hitsIcon; i++)
{
hitsIcon = target == this.icons[i].node || target.parentNode == this.icons[i].node;
}
if (!hitsIcon)
{
this.updateIcons(this.currentState, this.icons, me);
}
}
}
else
{
this.constraintHandler.reset();
}
};
/**
* Function: updateEdgeState
*
* Updates <edgeState>.
*/
mxConnectionHandler.prototype.updateEdgeState = function(current, constraint)
{
// TODO: Use generic method for writing constraint to style
if (this.sourceConstraint != null && this.sourceConstraint.point != null)
{
this.edgeState.style[mxConstants.STYLE_EXIT_X] = this.sourceConstraint.point.x;
this.edgeState.style[mxConstants.STYLE_EXIT_Y] = this.sourceConstraint.point.y;
}
if (constraint != null && constraint.point != null)
{
this.edgeState.style[mxConstants.STYLE_ENTRY_X] = constraint.point.x;
this.edgeState.style[mxConstants.STYLE_ENTRY_Y] = constraint.point.y;
}
else
{
delete this.edgeState.style[mxConstants.STYLE_ENTRY_X];
delete this.edgeState.style[mxConstants.STYLE_ENTRY_Y];
}
this.edgeState.absolutePoints = [null, (this.currentState != null) ? null : current];
this.graph.view.updateFixedTerminalPoint(this.edgeState, this.previous, true, this.sourceConstraint);
if (this.currentState != null)
{
if (constraint == null)
{
constraint = this.graph.getConnectionConstraint(this.edgeState, this.previous, false);
}
this.edgeState.setAbsoluteTerminalPoint(null, false);
this.graph.view.updateFixedTerminalPoint(this.edgeState, this.currentState, false, constraint);
}
// Scales and translates the waypoints to the model
var realPoints = null;
if (this.waypoints != null)
{
realPoints = [];
for (var i = 0; i < this.waypoints.length; i++)
{
var pt = this.waypoints[i].clone();
this.convertWaypoint(pt);
realPoints[i] = pt;
}
}
this.graph.view.updatePoints(this.edgeState, realPoints, this.previous, this.currentState);
this.graph.view.updateFloatingTerminalPoints(this.edgeState, this.previous, this.currentState);
};
/**
* Function: getTargetPerimeterPoint
*
* Returns the perimeter point for the given target state.
*
* Parameters:
*
* state - <mxCellState> that represents the target cell state.
* me - <mxMouseEvent> that represents the mouse move.
*/
mxConnectionHandler.prototype.getTargetPerimeterPoint = function(state, me)
{
var result = null;
var view = state.view;
var targetPerimeter = view.getPerimeterFunction(state);
if (targetPerimeter != null)
{
var next = (this.waypoints != null && this.waypoints.length > 0) ?
this.waypoints[this.waypoints.length - 1] :
new mxPoint(this.previous.getCenterX(), this.previous.getCenterY());
var tmp = targetPerimeter(view.getPerimeterBounds(state),
this.edgeState, next, false);
if (tmp != null)
{
result = tmp;
}
}
else
{
result = new mxPoint(state.getCenterX(), state.getCenterY());
}
return result;
};
/**
* Function: getSourcePerimeterPoint
*
* Hook to update the icon position(s) based on a mouseOver event. This is
* an empty implementation.
*
* Parameters:
*
* state - <mxCellState> that represents the target cell state.
* next - <mxPoint> that represents the next point along the previewed edge.
* me - <mxMouseEvent> that represents the mouse move.
*/
mxConnectionHandler.prototype.getSourcePerimeterPoint = function(state, next, me)
{
var result = null;
var view = state.view;
var sourcePerimeter = view.getPerimeterFunction(state);
var c = new mxPoint(state.getCenterX(), state.getCenterY());
if (sourcePerimeter != null)
{
var theta = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0);
var rad = -theta * (Math.PI / 180);
if (theta != 0)
{
next = mxUtils.getRotatedPoint(new mxPoint(next.x, next.y), Math.cos(rad), Math.sin(rad), c);
}
var tmp = sourcePerimeter(view.getPerimeterBounds(state), state, next, false);
if (tmp != null)
{
if (theta != 0)
{
tmp = mxUtils.getRotatedPoint(new mxPoint(tmp.x, tmp.y), Math.cos(-rad), Math.sin(-rad), c);
}
result = tmp;
}
}
else
{
result = c;
}
return result;
};
/**
* Function: updateIcons
*
* Hook to update the icon position(s) based on a mouseOver event. This is
* an empty implementation.
*
* Parameters:
*
* state - <mxCellState> under the mouse.
* icons - Array of currently displayed icons.
* me - <mxMouseEvent> that contains the mouse event.
*/
mxConnectionHandler.prototype.updateIcons = function(state, icons, me)
{
// empty
};
/**
* Function: isStopEvent
*
* Returns true if the given mouse up event should stop this handler. The
* connection will be created if <error> is null. Note that this is only
* called if <waypointsEnabled> is true. This implemtation returns true
* if there is a cell state in the given event.
*/
mxConnectionHandler.prototype.isStopEvent = function(me)
{
return me.getState() != null;
};
/**
* Function: addWaypoint
*
* Adds the waypoint for the given event to <waypoints>.
*/
mxConnectionHandler.prototype.addWaypointForEvent = function(me)
{
var point = mxUtils.convertPoint(this.graph.container, me.getX(), me.getY());
var dx = Math.abs(point.x - this.first.x);
var dy = Math.abs(point.y - this.first.y);
var addPoint = this.waypoints != null || (this.mouseDownCounter > 1 &&
(dx > this.graph.tolerance || dy > this.graph.tolerance));
if (addPoint)
{
if (this.waypoints == null)
{
this.waypoints = [];
}
var scale = this.graph.view.scale;
var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale,
this.graph.snap(me.getGraphY() / scale) * scale);
this.waypoints.push(point);
}
};
/**
* Function: checkConstraints
*
* Returns true if the connection for the given constraints is valid. This
* implementation returns true if the constraints are not pointing to the
* same fixed connection point.
*/
mxConnectionHandler.prototype.checkConstraints = function(c1, c2)
{
return (c1 == null || c2 == null || c1.point == null || c2.point == null ||
!c1.point.equals(c2.point) || c1.dx != c2.dx || c1.dy != c2.dy ||
c1.perimeter != c2.perimeter);
};
/**
* Function: mouseUp
*
* Handles the event by inserting the new connection.
*/
mxConnectionHandler.prototype.mouseUp = function(sender, me)
{
if (!me.isConsumed() && this.isConnecting())
{
if (this.waypointsEnabled && !this.isStopEvent(me))
{
this.addWaypointForEvent(me);
me.consume();
return;
}
var c1 = this.sourceConstraint;
var c2 = this.constraintHandler.currentConstraint;
var source = (this.previous != null) ? this.previous.cell : null;
var target = null;
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null)
{
target = this.constraintHandler.currentFocus.cell;
}
if (target == null && this.currentState != null)
{
target = this.currentState.cell;
}
// Inserts the edge if no validation error exists and if constraints differ
if (this.error == null && (source == null || target == null ||
source != target || this.checkConstraints(c1, c2)))
{
this.connect(source, target, me.getEvent(), me.getCell());
}
else
{
// Selects the source terminal for self-references
if (this.previous != null && this.marker.validState != null &&
this.previous.cell == this.marker.validState.cell)
{
this.graph.selectCellForEvent(this.marker.source, me.getEvent());
}
// Displays the error message if it is not an empty string,
// for empty error messages, the event is silently dropped
if (this.error != null && this.error.length > 0)
{
this.graph.validationAlert(this.error);
}
}
// Redraws the connect icons and resets the handler state
this.destroyIcons();
me.consume();
}
if (this.first != null)
{
this.reset();
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxConnectionHandler.prototype.reset = function()
{
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
// Resets the cursor on the container
if (this.cursor != null && this.graph.container != null)
{
this.graph.container.style.cursor = '';
}
this.destroyIcons();
this.marker.reset();
this.constraintHandler.reset();
this.originalPoint = null;
this.currentPoint = null;
this.edgeState = null;
this.previous = null;
this.error = null;
this.sourceConstraint = null;
this.mouseDownCounter = 0;
this.first = null;
this.fireEvent(new mxEventObject(mxEvent.RESET));
};
/**
* Function: drawPreview
*
* Redraws the preview edge using the color and width returned by
* <getEdgeColor> and <getEdgeWidth>.
*/
mxConnectionHandler.prototype.drawPreview = function()
{
this.updatePreview(this.error == null);
this.shape.redraw();
};
/**
* Function: getEdgeColor
*
* Returns the color used to draw the preview edge. This returns green if
* there is no edge validation error and red otherwise.
*
* Parameters:
*
* valid - Boolean indicating if the color for a valid edge should be
* returned.
*/
mxConnectionHandler.prototype.updatePreview = function(valid)
{
this.shape.strokewidth = this.getEdgeWidth(valid);
this.shape.stroke = this.getEdgeColor(valid);
};
/**
* Function: getEdgeColor
*
* Returns the color used to draw the preview edge. This returns green if
* there is no edge validation error and red otherwise.
*
* Parameters:
*
* valid - Boolean indicating if the color for a valid edge should be
* returned.
*/
mxConnectionHandler.prototype.getEdgeColor = function(valid)
{
return (valid) ? mxConstants.VALID_COLOR : mxConstants.INVALID_COLOR;
};
/**
* Function: getEdgeWidth
*
* Returns the width used to draw the preview edge. This returns 3 if
* there is no edge validation error and 1 otherwise.
*
* Parameters:
*
* valid - Boolean indicating if the width for a valid edge should be
* returned.
*/
mxConnectionHandler.prototype.getEdgeWidth = function(valid)
{
return (valid) ? 3 : 1;
};
/**
* Function: connect
*
* Connects the given source and target using a new edge. This
* implementation uses <createEdge> to create the edge.
*
* Parameters:
*
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
* evt - Mousedown event of the connect gesture.
* dropTarget - <mxCell> that represents the cell under the mouse when it was
* released.
*/
mxConnectionHandler.prototype.connect = function(source, target, evt, dropTarget)
{
if (target != null || this.isCreateTarget(evt) || this.graph.allowDanglingEdges)
{
// Uses the common parent of source and target or
// the default parent to insert the edge
var model = this.graph.getModel();
var terminalInserted = false;
var edge = null;
model.beginUpdate();
try
{
if (source != null && target == null && !this.graph.isIgnoreTerminalEvent(evt) && this.isCreateTarget(evt))
{
target = this.createTargetVertex(evt, source);
if (target != null)
{
dropTarget = this.graph.getDropTarget([target], evt, dropTarget);
terminalInserted = true;
// Disables edges as drop targets if the target cell was created
// FIXME: Should not shift if vertex was aligned (same in Java)
if (dropTarget == null || !this.graph.getModel().isEdge(dropTarget))
{
var pstate = this.graph.getView().getState(dropTarget);
if (pstate != null)
{
var tmp = model.getGeometry(target);
tmp.x -= pstate.origin.x;
tmp.y -= pstate.origin.y;
}
}
else
{
dropTarget = this.graph.getDefaultParent();
}
this.graph.addCell(target, dropTarget);
}
}
var parent = this.graph.getDefaultParent();
if (source != null && target != null &&
model.getParent(source) == model.getParent(target) &&
model.getParent(model.getParent(source)) != model.getRoot())
{
parent = model.getParent(source);
if ((source.geometry != null && source.geometry.relative) &&
(target.geometry != null && target.geometry.relative))
{
parent = model.getParent(parent);
}
}
// Uses the value of the preview edge state for inserting
// the new edge into the graph
var value = null;
var style = null;
if (this.edgeState != null)
{
value = this.edgeState.cell.value;
style = this.edgeState.cell.style;
}
edge = this.insertEdge(parent, null, value, source, target, style);
if (edge != null)
{
// Updates the connection constraints
this.graph.setConnectionConstraint(edge, source, true, this.sourceConstraint);
this.graph.setConnectionConstraint(edge, target, false, this.constraintHandler.currentConstraint);
// Uses geometry of the preview edge state
if (this.edgeState != null)
{
model.setGeometry(edge, this.edgeState.cell.geometry);
}
var parent = model.getParent(source);
// Inserts edge before source
if (this.isInsertBefore(edge, source, target, evt, dropTarget))
{
var index = null;
var tmp = source;
while (tmp.parent != null && tmp.geometry != null &&
tmp.geometry.relative && tmp.parent != edge.parent)
{
tmp = this.graph.model.getParent(tmp);
}
if (tmp != null && tmp.parent != null && tmp.parent == edge.parent)
{
model.add(parent, edge, tmp.parent.getIndex(tmp));
}
}
// Makes sure the edge has a non-null, relative geometry
var geo = model.getGeometry(edge);
if (geo == null)
{
geo = new mxGeometry();
geo.relative = true;
model.setGeometry(edge, geo);
}
// Uses scaled waypoints in geometry
if (this.waypoints != null && this.waypoints.length > 0)
{
var s = this.graph.view.scale;
var tr = this.graph.view.translate;
geo.points = [];
for (var i = 0; i < this.waypoints.length; i++)
{
var pt = this.waypoints[i];
geo.points.push(new mxPoint(pt.x / s - tr.x, pt.y / s - tr.y));
}
}
if (target == null)
{
var t = this.graph.view.translate;
var s = this.graph.view.scale;
var pt = (this.originalPoint != null) ?
new mxPoint(this.originalPoint.x / s - t.x, this.originalPoint.y / s - t.y) :
new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y);
pt.x -= this.graph.panDx / this.graph.view.scale;
pt.y -= this.graph.panDy / this.graph.view.scale;
geo.setTerminalPoint(pt, false);
}
this.fireEvent(new mxEventObject(mxEvent.CONNECT, 'cell', edge, 'terminal', target,
'event', evt, 'target', dropTarget, 'terminalInserted', terminalInserted));
}
}
catch (e)
{
mxLog.show();
mxLog.debug(e.message);
}
finally
{
model.endUpdate();
}
if (this.select)
{
this.selectCells(edge, (terminalInserted) ? target : null);
}
}
};
/**
* Function: selectCells
*
* Selects the given edge after adding a new connection. The target argument
* contains the target vertex if one has been inserted.
*/
mxConnectionHandler.prototype.selectCells = function(edge, target)
{
this.graph.setSelectionCell(edge);
};
/**
* Function: insertEdge
*
* Creates, inserts and returns the new edge for the given parameters. This
* implementation does only use <createEdge> if <factoryMethod> is defined,
* otherwise <mxGraph.insertEdge> will be used.
*/
mxConnectionHandler.prototype.insertEdge = function(parent, id, value, source, target, style)
{
if (this.factoryMethod == null)
{
return this.graph.insertEdge(parent, id, value, source, target, style);
}
else
{
var edge = this.createEdge(value, source, target, style);
edge = this.graph.addEdge(edge, parent, source, target);
return edge;
}
};
/**
* Function: createTargetVertex
*
* Hook method for creating new vertices on the fly if no target was
* under the mouse. This is only called if <createTarget> is true and
* returns null.
*
* Parameters:
*
* evt - Mousedown event of the connect gesture.
* source - <mxCell> that represents the source terminal.
*/
mxConnectionHandler.prototype.createTargetVertex = function(evt, source)
{
// Uses the first non-relative source
var geo = this.graph.getCellGeometry(source);
while (geo != null && geo.relative)
{
source = this.graph.getModel().getParent(source);
geo = this.graph.getCellGeometry(source);
}
var clone = this.graph.cloneCell(source);
var geo = this.graph.getModel().getGeometry(clone);
if (geo != null)
{
var t = this.graph.view.translate;
var s = this.graph.view.scale;
var point = new mxPoint(this.currentPoint.x / s - t.x, this.currentPoint.y / s - t.y);
geo.x = Math.round(point.x - geo.width / 2 - this.graph.panDx / s);
geo.y = Math.round(point.y - geo.height / 2 - this.graph.panDy / s);
// Aligns with source if within certain tolerance
var tol = this.getAlignmentTolerance();
if (tol > 0)
{
var sourceState = this.graph.view.getState(source);
if (sourceState != null)
{
var x = sourceState.x / s - t.x;
var y = sourceState.y / s - t.y;
if (Math.abs(x - geo.x) <= tol)
{
geo.x = Math.round(x);
}
if (Math.abs(y - geo.y) <= tol)
{
geo.y = Math.round(y);
}
}
}
}
return clone;
};
/**
* Function: getAlignmentTolerance
*
* Returns the tolerance for aligning new targets to sources. This returns the grid size / 2.
*/
mxConnectionHandler.prototype.getAlignmentTolerance = function(evt)
{
return (this.graph.isGridEnabled()) ? this.graph.gridSize / 2 : this.graph.tolerance;
};
/**
* Function: createEdge
*
* Creates and returns a new edge using <factoryMethod> if one exists. If
* no factory method is defined, then a new default edge is returned. The
* source and target arguments are informal, the actual connection is
* setup later by the caller of this function.
*
* Parameters:
*
* value - Value to be used for creating the edge.
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
* style - Optional style from the preview edge.
*/
mxConnectionHandler.prototype.createEdge = function(value, source, target, style)
{
var edge = null;
// Creates a new edge using the factoryMethod
if (this.factoryMethod != null)
{
edge = this.factoryMethod(source, target, style);
}
if (edge == null)
{
edge = new mxCell(value || '');
edge.setEdge(true);
edge.setStyle(style);
var geo = new mxGeometry();
geo.relative = true;
edge.setGeometry(geo);
}
return edge;
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes. This should be
* called on all instances. It is called automatically for the built-in
* instance created for each <mxGraph>.
*/
mxConnectionHandler.prototype.destroy = function()
{
this.graph.removeMouseListener(this);
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
if (this.marker != null)
{
this.marker.destroy();
this.marker = null;
}
if (this.constraintHandler != null)
{
this.constraintHandler.destroy();
this.constraintHandler = null;
}
if (this.changeHandler != null)
{
this.graph.getModel().removeListener(this.changeHandler);
this.graph.getView().removeListener(this.changeHandler);
this.changeHandler = null;
}
if (this.drillHandler != null)
{
this.graph.removeListener(this.drillHandler);
this.graph.getView().removeListener(this.drillHandler);
this.drillHandler = null;
}
if (this.escapeHandler != null)
{
this.graph.removeListener(this.escapeHandler);
this.escapeHandler = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConstraintHandler
*
* Handles constraints on connection targets. This class is in charge of
* showing fixed points when the mouse is over a vertex and handles constraints
* to establish new connections.
*
* Constructor: mxConstraintHandler
*
* Constructs an new constraint handler.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* factoryMethod - Optional function to create the edge. The function takes
* the source and target <mxCell> as the first and second argument and
* returns the <mxCell> that represents the new edge.
*/
function mxConstraintHandler(graph)
{
this.graph = graph;
// Adds a graph model listener to update the current focus on changes
this.resetHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null)
{
this.reset();
}
else
{
this.redraw();
}
});
this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler);
this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.resetHandler);
this.graph.view.addListener(mxEvent.TRANSLATE, this.resetHandler);
this.graph.view.addListener(mxEvent.SCALE, this.resetHandler);
this.graph.addListener(mxEvent.ROOT, this.resetHandler);
};
/**
* Variable: pointImage
*
* <mxImage> to be used as the image for fixed connection points.
*/
mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxConstraintHandler.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxConstraintHandler.prototype.enabled = true;
/**
* Variable: highlightColor
*
* Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>.
*/
mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxConstraintHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxConstraintHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxConstraintHandler.prototype.reset = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
}
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
this.currentConstraint = null;
this.currentFocusArea = null;
this.currentPoint = null;
this.currentFocus = null;
this.focusPoints = null;
};
/**
* Function: getTolerance
*
* Returns the tolerance to be used for intersecting connection points. This
* implementation returns <mxGraph.tolerance>.
*
* Parameters:
*
* me - <mxMouseEvent> whose tolerance should be returned.
*/
mxConstraintHandler.prototype.getTolerance = function(me)
{
return this.graph.getTolerance();
};
/**
* Function: getImageForConstraint
*
* Returns the tolerance to be used for intersecting connection points.
*/
mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point)
{
return this.pointImage;
};
/**
* Function: isEventIgnored
*
* Returns true if the given <mxMouseEvent> should be ignored in <update>. This
* implementation always returns false.
*/
mxConstraintHandler.prototype.isEventIgnored = function(me, source)
{
return false;
};
/**
* Function: isStateIgnored
*
* Returns true if the given state should be ignored. This always returns false.
*/
mxConstraintHandler.prototype.isStateIgnored = function(state, source)
{
return false;
};
/**
* Function: destroyIcons
*
* Destroys the <focusIcons> if they exist.
*/
mxConstraintHandler.prototype.destroyIcons = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
};
/**
* Function: destroyFocusHighlight
*
* Destroys the <focusHighlight> if one exists.
*/
mxConstraintHandler.prototype.destroyFocusHighlight = function()
{
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
};
/**
* Function: isKeepFocusEvent
*
* Returns true if the current focused state should not be changed for the given event.
* This returns true if shift and alt are pressed.
*/
mxConstraintHandler.prototype.isKeepFocusEvent = function(me)
{
return mxEvent.isShiftDown(me.getEvent());
};
/**
* Function: getCellForEvent
*
* Returns the cell for the given event.
*/
mxConstraintHandler.prototype.getCellForEvent = function(me, point)
{
var cell = me.getCell();
// Gets cell under actual point if different from event location
if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y))
{
cell = this.graph.getCellAt(point.x, point.y);
}
// Uses connectable parent vertex if one exists
if (cell != null && !this.graph.isCellConnectable(cell))
{
var parent = this.graph.getModel().getParent(cell);
if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
{
cell = parent;
}
}
return (this.graph.isCellLocked(cell)) ? null : cell;
};
/**
* Function: update
*
* Updates the state of this handler based on the given <mxMouseEvent>.
* Source is a boolean indicating if the cell is a source or target.
*/
mxConstraintHandler.prototype.update = function(me, source, existingEdge, point)
{
if (this.isEnabled() && !this.isEventIgnored(me))
{
// Lazy installation of mouseleave handler
if (this.mouseleaveHandler == null && this.graph.container != null)
{
this.mouseleaveHandler = mxUtils.bind(this, function()
{
this.reset();
});
mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler);
}
var tol = this.getTolerance(me);
var x = (point != null) ? point.x : me.getGraphX();
var y = (point != null) ? point.y : me.getGraphY();
var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol);
var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol);
var state = this.graph.view.getState(this.getCellForEvent(me, point));
// Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed
if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null ||
(state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) ||
!mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus))
{
this.currentFocusArea = null;
this.currentFocus = null;
this.setFocus(me, state, source);
}
this.currentConstraint = null;
this.currentPoint = null;
var minDistSq = null;
if (this.focusIcons != null && this.constraints != null &&
(state == null || this.currentFocus == state))
{
var cx = mouse.getCenterX();
var cy = mouse.getCenterY();
for (var i = 0; i < this.focusIcons.length; i++)
{
var dx = cx - this.focusIcons[i].bounds.getCenterX();
var dy = cy - this.focusIcons[i].bounds.getCenterY();
var tmp = dx * dx + dy * dy;
if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null &&
this.intersects(this.focusIcons[i], grid, source, existingEdge))) &&
(minDistSq == null || tmp < minDistSq))
{
this.currentConstraint = this.constraints[i];
this.currentPoint = this.focusPoints[i];
minDistSq = tmp;
var tmp = this.focusIcons[i].bounds.clone();
tmp.grow(mxConstants.HIGHLIGHT_SIZE + 1);
tmp.width -= 1;
tmp.height -= 1;
if (this.focusHighlight == null)
{
var hl = this.createHighlightShape();
hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML;
hl.pointerEvents = false;
hl.init(this.graph.getView().getOverlayPane());
this.focusHighlight = hl;
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
mxEvent.redirectMouseEvents(hl.node, this.graph, getState);
}
this.focusHighlight.bounds = tmp;
this.focusHighlight.redraw();
}
}
}
if (this.currentConstraint == null)
{
this.destroyFocusHighlight();
}
}
else
{
this.currentConstraint = null;
this.currentFocus = null;
this.currentPoint = null;
}
};
/**
* Function: redraw
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.redraw = function()
{
if (this.currentFocus != null && this.constraints != null && this.focusIcons != null)
{
var state = this.graph.view.getState(this.currentFocus.cell);
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
this.focusIcons[i].bounds = bounds;
this.focusIcons[i].redraw();
this.currentFocusArea.add(this.focusIcons[i].bounds);
this.focusPoints[i] = cp;
}
}
};
/**
* Function: setFocus
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.setFocus = function(me, state, source)
{
this.constraints = (state != null && !this.isStateIgnored(state, source) &&
this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ?
(this.graph.getAllConnectionConstraints(state, source) || []) : []) : null;
// Only uses cells which have constraints
if (this.constraints != null)
{
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
this.focusPoints = [];
this.focusIcons = [];
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var src = img.src;
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
var icon = new mxImageShape(bounds, src);
icon.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
icon.preserveImageAspect = false;
icon.init(this.graph.getView().getDecoratorPane());
// Fixes lost event tracking for images in quirks / IE8 standards
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
mxEvent.addListener(icon.node, 'dragstart', function(evt)
{
mxEvent.consume(evt);
return false;
});
}
// Move the icon behind all other overlays
if (icon.node.previousSibling != null)
{
icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
}
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
icon.redraw();
mxEvent.redirectMouseEvents(icon.node, this.graph, getState);
this.currentFocusArea.add(icon.bounds);
this.focusIcons.push(icon);
this.focusPoints.push(cp);
}
this.currentFocusArea.grow(this.getTolerance(me));
}
else
{
this.destroyIcons();
this.destroyFocusHighlight();
}
};
/**
* Function: createHighlightShape
*
* Create the shape used to paint the highlight.
*
* Returns true if the given icon intersects the given point.
*/
mxConstraintHandler.prototype.createHighlightShape = function()
{
var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH);
hl.opacity = mxConstants.HIGHLIGHT_OPACITY;
return hl;
};
/**
* Function: intersects
*
* Returns true if the given icon intersects the given rectangle.
*/
mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge)
{
return mxUtils.intersects(icon.bounds, mouse);
};
/**
* Function: destroy
*
* Destroy this handler.
*/
mxConstraintHandler.prototype.destroy = function()
{
this.reset();
if (this.resetHandler != null)
{
this.graph.model.removeListener(this.resetHandler);
this.graph.view.removeListener(this.resetHandler);
this.graph.removeListener(this.resetHandler);
this.resetHandler = null;
}
if (this.mouseleaveHandler != null && this.graph.container != null)
{
mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler);
this.mouseleaveHandler = null;
}
};
/**
* Copyright (c) 2006-2016, JGraph Ltd
* Copyright (c) 2006-2016, Gaudenz Alder
*/
/**
* Class: mxRubberband
*
* Event handler that selects rectangular regions. This is not built-into
* <mxGraph>. To enable rubberband selection in a graph, use the following code.
*
* Example:
*
* (code)
* var rubberband = new mxRubberband(graph);
* (end)
*
* Constructor: mxRubberband
*
* Constructs an event handler that selects rectangular regions in the graph
* using rubberband selection.
*/
function mxRubberband(graph)
{
if (graph != null)
{
this.graph = graph;
this.graph.addMouseListener(this);
// Handles force rubberband event
this.forceRubberbandHandler = mxUtils.bind(this, function(sender, evt)
{
var evtName = evt.getProperty('eventName');
var me = evt.getProperty('event');
if (evtName == mxEvent.MOUSE_DOWN && this.isForceRubberbandEvent(me))
{
var offset = mxUtils.getOffset(this.graph.container);
var origin = mxUtils.getScrollOrigin(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
this.start(me.getX() + origin.x, me.getY() + origin.y);
me.consume(false);
}
});
this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.forceRubberbandHandler);
// Repaints the marquee after autoscroll
this.panHandler = mxUtils.bind(this, function()
{
this.repaint();
});
this.graph.addListener(mxEvent.PAN, this.panHandler);
// Does not show menu if any touch gestures take place after the trigger
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
{
if (this.first != null)
{
this.reset();
}
});
this.graph.addListener(mxEvent.GESTURE, this.gestureHandler);
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload',
mxUtils.bind(this, function()
{
this.destroy();
})
);
}
}
};
/**
* Variable: defaultOpacity
*
* Specifies the default opacity to be used for the rubberband div. Default
* is 20.
*/
mxRubberband.prototype.defaultOpacity = 20;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxRubberband.prototype.enabled = true;
/**
* Variable: div
*
* Holds the DIV element which is currently visible.
*/
mxRubberband.prototype.div = null;
/**
* Variable: sharedDiv
*
* Holds the DIV element which is used to display the rubberband.
*/
mxRubberband.prototype.sharedDiv = null;
/**
* Variable: currentX
*
* Holds the value of the x argument in the last call to <update>.
*/
mxRubberband.prototype.currentX = 0;
/**
* Variable: currentY
*
* Holds the value of the y argument in the last call to <update>.
*/
mxRubberband.prototype.currentY = 0;
/**
* Variable: fadeOut
*
* Optional fade out effect. Default is false.
*/
mxRubberband.prototype.fadeOut = false;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation returns
* <enabled>.
*/
mxRubberband.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation updates
* <enabled>.
*/
mxRubberband.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isForceRubberbandEvent
*
* Returns true if the given <mxMouseEvent> should start rubberband selection.
* This implementation returns true if the alt key is pressed.
*/
mxRubberband.prototype.isForceRubberbandEvent = function(me)
{
return mxEvent.isAltDown(me.getEvent());
};
/**
* Function: mouseDown
*
* Handles the event by initiating a rubberband selection. By consuming the
* event all subsequent events of the gesture are redirected to this
* handler.
*/
mxRubberband.prototype.mouseDown = function(sender, me)
{
if (!me.isConsumed() && this.isEnabled() && this.graph.isEnabled() &&
me.getState() == null && !mxEvent.isMultiTouchEvent(me.getEvent()))
{
var offset = mxUtils.getOffset(this.graph.container);
var origin = mxUtils.getScrollOrigin(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
this.start(me.getX() + origin.x, me.getY() + origin.y);
// Does not prevent the default for this event so that the
// event processing chain is still executed even if we start
// rubberbanding. This is required eg. in ExtJs to hide the
// current context menu. In mouseMove we'll make sure we're
// not selecting anything while we're rubberbanding.
me.consume(false);
}
};
/**
* Function: start
*
* Sets the start point for the rubberband selection.
*/
mxRubberband.prototype.start = function(x, y)
{
this.first = new mxPoint(x, y);
var container = this.graph.container;
function createMouseEvent(evt)
{
var me = new mxMouseEvent(evt);
var pt = mxUtils.convertPoint(container, me.getX(), me.getY());
me.graphX = pt.x;
me.graphY = pt.y;
return me;
};
this.dragHandler = mxUtils.bind(this, function(evt)
{
this.mouseMove(this.graph, createMouseEvent(evt));
});
this.dropHandler = mxUtils.bind(this, function(evt)
{
this.mouseUp(this.graph, createMouseEvent(evt));
});
// Workaround for rubberband stopping if the mouse leaves the container in Firefox
if (mxClient.IS_FF)
{
mxEvent.addGestureListeners(document, null, this.dragHandler, this.dropHandler);
}
};
/**
* Function: mouseMove
*
* Handles the event by updating therubberband selection.
*/
mxRubberband.prototype.mouseMove = function(sender, me)
{
if (!me.isConsumed() && this.first != null)
{
var origin = mxUtils.getScrollOrigin(this.graph.container);
var offset = mxUtils.getOffset(this.graph.container);
origin.x -= offset.x;
origin.y -= offset.y;
var x = me.getX() + origin.x;
var y = me.getY() + origin.y;
var dx = this.first.x - x;
var dy = this.first.y - y;
var tol = this.graph.tolerance;
if (this.div != null || Math.abs(dx) > tol || Math.abs(dy) > tol)
{
if (this.div == null)
{
this.div = this.createShape();
}
// Clears selection while rubberbanding. This is required because
// the event is not consumed in mouseDown.
mxUtils.clearSelection();
this.update(x, y);
me.consume();
}
}
};
/**
* Function: createShape
*
* Creates the rubberband selection shape.
*/
mxRubberband.prototype.createShape = function()
{
if (this.sharedDiv == null)
{
this.sharedDiv = document.createElement('div');
this.sharedDiv.className = 'mxRubberband';
mxUtils.setOpacity(this.sharedDiv, this.defaultOpacity);
}
this.graph.container.appendChild(this.sharedDiv);
var result = this.sharedDiv;
if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut)
{
this.sharedDiv = null;
}
return result;
};
/**
* Function: isActive
*
* Returns true if this handler is active.
*/
mxRubberband.prototype.isActive = function(sender, me)
{
return this.div != null && this.div.style.display != 'none';
};
/**
* Function: mouseUp
*
* Handles the event by selecting the region of the rubberband using
* <mxGraph.selectRegion>.
*/
mxRubberband.prototype.mouseUp = function(sender, me)
{
var active = this.isActive();
this.reset();
if (active)
{
this.execute(me.getEvent());
me.consume();
}
};
/**
* Function: execute
*
* Resets the state of this handler and selects the current region
* for the given event.
*/
mxRubberband.prototype.execute = function(evt)
{
var rect = new mxRectangle(this.x, this.y, this.width, this.height);
this.graph.selectRegion(rect, evt);
};
/**
* Function: reset
*
* Resets the state of the rubberband selection.
*/
mxRubberband.prototype.reset = function()
{
if (this.div != null)
{
if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10) && this.fadeOut)
{
var temp = this.div;
mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s linear');
temp.style.pointerEvents = 'none';
temp.style.opacity = 0;
window.setTimeout(function()
{
temp.parentNode.removeChild(temp);
}, 200);
}
else
{
this.div.parentNode.removeChild(this.div);
}
}
mxEvent.removeGestureListeners(document, null, this.dragHandler, this.dropHandler);
this.dragHandler = null;
this.dropHandler = null;
this.currentX = 0;
this.currentY = 0;
this.first = null;
this.div = null;
};
/**
* Function: update
*
* Sets <currentX> and <currentY> and calls <repaint>.
*/
mxRubberband.prototype.update = function(x, y)
{
this.currentX = x;
this.currentY = y;
this.repaint();
};
/**
* Function: repaint
*
* Computes the bounding box and updates the style of the <div>.
*/
mxRubberband.prototype.repaint = function()
{
if (this.div != null)
{
var x = this.currentX - this.graph.panDx;
var y = this.currentY - this.graph.panDy;
this.x = Math.min(this.first.x, x);
this.y = Math.min(this.first.y, y);
this.width = Math.max(this.first.x, x) - this.x;
this.height = Math.max(this.first.y, y) - this.y;
var dx = (mxClient.IS_VML) ? this.graph.panDx : 0;
var dy = (mxClient.IS_VML) ? this.graph.panDy : 0;
this.div.style.left = (this.x + dx) + 'px';
this.div.style.top = (this.y + dy) + 'px';
this.div.style.width = Math.max(1, this.width) + 'px';
this.div.style.height = Math.max(1, this.height) + 'px';
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes. This does
* normally not need to be called, it is called automatically when the
* window unloads.
*/
mxRubberband.prototype.destroy = function()
{
if (!this.destroyed)
{
this.destroyed = true;
this.graph.removeMouseListener(this);
this.graph.removeListener(this.forceRubberbandHandler);
this.graph.removeListener(this.panHandler);
this.reset();
if (this.sharedDiv != null)
{
this.sharedDiv = null;
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxHandle
*
* Implements a single custom handle for vertices.
*
* Constructor: mxHandle
*
* Constructs a new handle for the given state.
*
* Parameters:
*
* state - <mxCellState> of the cell to be handled.
*/
function mxHandle(state, cursor, image)
{
this.graph = state.view.graph;
this.state = state;
this.cursor = (cursor != null) ? cursor : this.cursor;
this.image = (image != null) ? image : this.image;
this.init();
};
/**
* Variable: cursor
*
* Specifies the cursor to be used for this handle. Default is 'default'.
*/
mxHandle.prototype.cursor = 'default';
/**
* Variable: image
*
* Specifies the <mxImage> to be used to render the handle. Default is null.
*/
mxHandle.prototype.image = null;
/**
* Variable: ignoreGrid
*
* Default is false.
*/
mxHandle.prototype.ignoreGrid = false;
/**
* Function: getPosition
*
* Hook for subclassers to return the current position of the handle.
*/
mxHandle.prototype.getPosition = function(bounds) { };
/**
* Function: setPosition
*
* Hooks for subclassers to update the style in the <state>.
*/
mxHandle.prototype.setPosition = function(bounds, pt, me) { };
/**
* Function: execute
*
* Hook for subclassers to execute the handle.
*/
mxHandle.prototype.execute = function() { };
/**
* Function: copyStyle
*
* Sets the cell style with the given name to the corresponding value in <state>.
*/
mxHandle.prototype.copyStyle = function(key)
{
this.graph.setCellStyles(key, this.state.style[key], [this.state.cell]);
};
/**
* Function: processEvent
*
* Processes the given <mxMouseEvent> and invokes <setPosition>.
*/
mxHandle.prototype.processEvent = function(me)
{
var scale = this.graph.view.scale;
var tr = this.graph.view.translate;
var pt = new mxPoint(me.getGraphX() / scale - tr.x, me.getGraphY() / scale - tr.y);
// Center shape on mouse cursor
if (this.shape != null && this.shape.bounds != null)
{
pt.x -= this.shape.bounds.width / scale / 4;
pt.y -= this.shape.bounds.height / scale / 4;
}
// Snaps to grid for the rotated position then applies the rotation for the direction after that
var alpha1 = -mxUtils.toRadians(this.getRotation());
var alpha2 = -mxUtils.toRadians(this.getTotalRotation()) - alpha1;
pt = this.flipPoint(this.rotatePoint(this.snapPoint(this.rotatePoint(pt, alpha1),
this.ignoreGrid || !this.graph.isGridEnabledEvent(me.getEvent())), alpha2));
this.setPosition(this.state.getPaintBounds(), pt, me);
this.positionChanged();
this.redraw();
};
/**
* Function: positionChanged
*
* Called after <setPosition> has been called in <processEvent>. This repaints
* the state using <mxCellRenderer>.
*/
mxHandle.prototype.positionChanged = function()
{
if (this.state.text != null)
{
this.state.text.apply(this.state);
}
if (this.state.shape != null)
{
this.state.shape.apply(this.state);
}
this.graph.cellRenderer.redraw(this.state, true);
};
/**
* Function: getRotation
*
* Returns the rotation defined in the style of the cell.
*/
mxHandle.prototype.getRotation = function()
{
if (this.state.shape != null)
{
return this.state.shape.getRotation();
}
return 0;
};
/**
* Function: getTotalRotation
*
* Returns the rotation from the style and the rotation from the direction of
* the cell.
*/
mxHandle.prototype.getTotalRotation = function()
{
if (this.state.shape != null)
{
return this.state.shape.getShapeRotation();
}
return 0;
};
/**
* Function: init
*
* Creates and initializes the shapes required for this handle.
*/
mxHandle.prototype.init = function()
{
var html = this.isHtmlRequired();
if (this.image != null)
{
this.shape = new mxImageShape(new mxRectangle(0, 0, this.image.width, this.image.height), this.image.src);
this.shape.preserveImageAspect = false;
}
else
{
this.shape = this.createShape(html);
}
this.initShape(html);
};
/**
* Function: createShape
*
* Creates and returns the shape for this handle.
*/
mxHandle.prototype.createShape = function(html)
{
var bounds = new mxRectangle(0, 0, mxConstants.HANDLE_SIZE, mxConstants.HANDLE_SIZE);
return new mxRectangleShape(bounds, mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
};
/**
* Function: initShape
*
* Initializes <shape> and sets its cursor.
*/
mxHandle.prototype.initShape = function(html)
{
if (html && this.shape.isHtmlAllowed())
{
this.shape.dialect = mxConstants.DIALECT_STRICTHTML;
this.shape.init(this.graph.container);
}
else
{
this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
if (this.cursor != null)
{
this.shape.init(this.graph.getView().getOverlayPane());
}
}
mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state);
this.shape.node.style.cursor = this.cursor;
};
/**
* Function: redraw
*
* Renders the shape for this handle.
*/
mxHandle.prototype.redraw = function()
{
if (this.shape != null && this.state.shape != null)
{
var pt = this.getPosition(this.state.getPaintBounds());
if (pt != null)
{
var alpha = mxUtils.toRadians(this.getTotalRotation());
pt = this.rotatePoint(this.flipPoint(pt), alpha);
var scale = this.graph.view.scale;
var tr = this.graph.view.translate;
this.shape.bounds.x = Math.floor((pt.x + tr.x) * scale - this.shape.bounds.width / 2);
this.shape.bounds.y = Math.floor((pt.y + tr.y) * scale - this.shape.bounds.height / 2);
// Needed to force update of text bounds
this.shape.redraw();
}
}
};
/**
* Function: isHtmlRequired
*
* Returns true if this handle should be rendered in HTML. This returns true if
* the text node is in the graph container.
*/
mxHandle.prototype.isHtmlRequired = function()
{
return this.state.text != null && this.state.text.node.parentNode == this.graph.container;
};
/**
* Function: rotatePoint
*
* Rotates the point by the given angle.
*/
mxHandle.prototype.rotatePoint = function(pt, alpha)
{
var bounds = this.state.getCellBounds();
var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY());
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
return mxUtils.getRotatedPoint(pt, cos, sin, cx);
};
/**
* Function: flipPoint
*
* Flips the given point vertically and/or horizontally.
*/
mxHandle.prototype.flipPoint = function(pt)
{
if (this.state.shape != null)
{
var bounds = this.state.getCellBounds();
if (this.state.shape.flipH)
{
pt.x = 2 * bounds.x + bounds.width - pt.x;
}
if (this.state.shape.flipV)
{
pt.y = 2 * bounds.y + bounds.height - pt.y;
}
}
return pt;
};
/**
* Function: snapPoint
*
* Snaps the given point to the grid if ignore is false. This modifies
* the given point in-place and also returns it.
*/
mxHandle.prototype.snapPoint = function(pt, ignore)
{
if (!ignore)
{
pt.x = this.graph.snap(pt.x);
pt.y = this.graph.snap(pt.y);
}
return pt;
};
/**
* Function: setVisible
*
* Shows or hides this handle.
*/
mxHandle.prototype.setVisible = function(visible)
{
if (this.shape != null && this.shape.node != null)
{
this.shape.node.style.display = (visible) ? '' : 'none';
}
};
/**
* Function: reset
*
* Resets the state of this handle by setting its visibility to true.
*/
mxHandle.prototype.reset = function()
{
this.setVisible(true);
this.state.style = this.graph.getCellStyle(this.state.cell);
this.positionChanged();
};
/**
* Function: destroy
*
* Destroys this handle.
*/
mxHandle.prototype.destroy = function()
{
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxVertexHandler
*
* Event handler for resizing cells. This handler is automatically created in
* <mxGraph.createHandler>.
*
* Constructor: mxVertexHandler
*
* Constructs an event handler that allows to resize vertices
* and groups.
*
* Parameters:
*
* state - <mxCellState> of the cell to be resized.
*/
function mxVertexHandler(state)
{
if (state != null)
{
this.state = state;
this.init();
// Handles escape keystrokes
this.escapeHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.livePreview && this.index != null)
{
// Redraws the live preview
this.state.view.graph.cellRenderer.redraw(this.state, true);
// Redraws connected edges
this.state.view.invalidate(this.state.cell);
this.state.invalid = false;
this.state.view.validate();
}
this.reset();
});
this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler);
}
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxVertexHandler.prototype.graph = null;
/**
* Variable: state
*
* Reference to the <mxCellState> being modified.
*/
mxVertexHandler.prototype.state = null;
/**
* Variable: singleSizer
*
* Specifies if only one sizer handle at the bottom, right corner should be
* used. Default is false.
*/
mxVertexHandler.prototype.singleSizer = false;
/**
* Variable: index
*
* Holds the index of the current handle.
*/
mxVertexHandler.prototype.index = null;
/**
* Variable: allowHandleBoundsCheck
*
* Specifies if the bounds of handles should be used for hit-detection in IE or
* if <tolerance> > 0. Default is true.
*/
mxVertexHandler.prototype.allowHandleBoundsCheck = true;
/**
* Variable: handleImage
*
* Optional <mxImage> to be used as handles. Default is null.
*/
mxVertexHandler.prototype.handleImage = null;
/**
* Variable: tolerance
*
* Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0.
*/
mxVertexHandler.prototype.tolerance = 0;
/**
* Variable: rotationEnabled
*
* Specifies if a rotation handle should be visible. Default is false.
*/
mxVertexHandler.prototype.rotationEnabled = false;
/**
* Variable: parentHighlightEnabled
*
* Specifies if the parent should be highlighted if a child cell is selected.
* Default is false.
*/
mxVertexHandler.prototype.parentHighlightEnabled = false;
/**
* Variable: rotationRaster
*
* Specifies if rotation steps should be "rasterized" depening on the distance
* to the handle. Default is true.
*/
mxVertexHandler.prototype.rotationRaster = true;
/**
* Variable: rotationCursor
*
* Specifies the cursor for the rotation handle. Default is 'crosshair'.
*/
mxVertexHandler.prototype.rotationCursor = 'crosshair';
/**
* Variable: livePreview
*
* Specifies if resize should change the cell in-place. This is an experimental
* feature for non-touch devices. Default is false.
*/
mxVertexHandler.prototype.livePreview = false;
/**
* Variable: manageSizers
*
* Specifies if sizers should be hidden and spaced if the vertex is small.
* Default is false.
*/
mxVertexHandler.prototype.manageSizers = false;
/**
* Variable: constrainGroupByChildren
*
* Specifies if the size of groups should be constrained by the children.
* Default is false.
*/
mxVertexHandler.prototype.constrainGroupByChildren = false;
/**
* Variable: rotationHandleVSpacing
*
* Vertical spacing for rotation icon. Default is -16.
*/
mxVertexHandler.prototype.rotationHandleVSpacing = -16;
/**
* Variable: horizontalOffset
*
* The horizontal offset for the handles. This is updated in <redrawHandles>
* if <manageSizers> is true and the sizers are offset horizontally.
*/
mxVertexHandler.prototype.horizontalOffset = 0;
/**
* Variable: verticalOffset
*
* The horizontal offset for the handles. This is updated in <redrawHandles>
* if <manageSizers> is true and the sizers are offset vertically.
*/
mxVertexHandler.prototype.verticalOffset = 0;
/**
* Function: init
*
* Initializes the shapes required for this vertex handler.
*/
mxVertexHandler.prototype.init = function()
{
this.graph = this.state.view.graph;
this.selectionBounds = this.getSelectionBounds(this.state);
this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, this.selectionBounds.width, this.selectionBounds.height);
this.selectionBorder = this.createSelectionShape(this.bounds);
// VML dialect required here for event transparency in IE
this.selectionBorder.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.selectionBorder.pointerEvents = false;
this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0');
this.selectionBorder.init(this.graph.getView().getOverlayPane());
mxEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state);
if (this.graph.isCellMovable(this.state.cell))
{
this.selectionBorder.setCursor(mxConstants.CURSOR_MOVABLE_VERTEX);
}
// Adds the sizer handles
if (mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells)
{
var resizable = this.graph.isCellResizable(this.state.cell);
this.sizers = [];
if (resizable || (this.graph.isLabelMovable(this.state.cell) &&
this.state.width >= 2 && this.state.height >= 2))
{
var i = 0;
if (resizable)
{
if (!this.singleSizer)
{
this.sizers.push(this.createSizer('nw-resize', i++));
this.sizers.push(this.createSizer('n-resize', i++));
this.sizers.push(this.createSizer('ne-resize', i++));
this.sizers.push(this.createSizer('w-resize', i++));
this.sizers.push(this.createSizer('e-resize', i++));
this.sizers.push(this.createSizer('sw-resize', i++));
this.sizers.push(this.createSizer('s-resize', i++));
}
this.sizers.push(this.createSizer('se-resize', i++));
}
var geo = this.graph.model.getGeometry(this.state.cell);
if (geo != null && !geo.relative && !this.graph.isSwimlane(this.state.cell) &&
this.graph.isLabelMovable(this.state.cell))
{
// Marks this as the label handle for getHandleForEvent
this.labelShape = this.createSizer(mxConstants.CURSOR_LABEL_HANDLE, mxEvent.LABEL_HANDLE, mxConstants.LABEL_HANDLE_SIZE, mxConstants.LABEL_HANDLE_FILLCOLOR);
this.sizers.push(this.labelShape);
}
}
else if (this.graph.isCellMovable(this.state.cell) && !this.graph.isCellResizable(this.state.cell) &&
this.state.width < 2 && this.state.height < 2)
{
this.labelShape = this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX,
mxEvent.LABEL_HANDLE, null, mxConstants.LABEL_HANDLE_FILLCOLOR);
this.sizers.push(this.labelShape);
}
}
// Adds the rotation handler
if (this.isRotationHandleVisible())
{
this.rotationShape = this.createSizer(this.rotationCursor, mxEvent.ROTATION_HANDLE,
mxConstants.HANDLE_SIZE + 3, mxConstants.HANDLE_FILLCOLOR);
this.sizers.push(this.rotationShape);
}
this.customHandles = this.createCustomHandles();
this.redraw();
if (this.constrainGroupByChildren)
{
this.updateMinBounds();
}
};
/**
* Function: isRotationHandleVisible
*
* Returns true if the rotation handle should be showing.
*/
mxVertexHandler.prototype.isRotationHandleVisible = function()
{
return this.graph.isEnabled() && this.rotationEnabled && this.graph.isCellRotatable(this.state.cell) &&
(mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells) &&
this.state.width >= 2 && this.state.height >= 2;
};
/**
* Function: isConstrainedEvent
*
* Returns true if the aspect ratio if the cell should be maintained.
*/
mxVertexHandler.prototype.isConstrainedEvent = function(me)
{
return mxEvent.isShiftDown(me.getEvent()) || this.state.style[mxConstants.STYLE_ASPECT] == 'fixed';
};
/**
* Function: isCenteredEvent
*
* Returns true if the center of the vertex should be maintained during the resize.
*/
mxVertexHandler.prototype.isCenteredEvent = function(state, me)
{
return false;
};
/**
* Function: createCustomHandles
*
* Returns an array of custom handles. This implementation returns null.
*/
mxVertexHandler.prototype.createCustomHandles = function()
{
return null;
};
/**
* Function: updateMinBounds
*
* Initializes the shapes required for this vertex handler.
*/
mxVertexHandler.prototype.updateMinBounds = function()
{
var children = this.graph.getChildCells(this.state.cell);
if (children.length > 0)
{
this.minBounds = this.graph.view.getBounds(children);
if (this.minBounds != null)
{
var s = this.state.view.scale;
var t = this.state.view.translate;
this.minBounds.x -= this.state.x;
this.minBounds.y -= this.state.y;
this.minBounds.x /= s;
this.minBounds.y /= s;
this.minBounds.width /= s;
this.minBounds.height /= s;
this.x0 = this.state.x / s - t.x;
this.y0 = this.state.y / s - t.y;
}
}
};
/**
* Function: getSelectionBounds
*
* Returns the mxRectangle that defines the bounds of the selection
* border.
*/
mxVertexHandler.prototype.getSelectionBounds = function(state)
{
return new mxRectangle(Math.round(state.x), Math.round(state.y), Math.round(state.width), Math.round(state.height));
};
/**
* Function: createParentHighlightShape
*
* Creates the shape used to draw the selection border.
*/
mxVertexHandler.prototype.createParentHighlightShape = function(bounds)
{
return this.createSelectionShape(bounds);
};
/**
* Function: createSelectionShape
*
* Creates the shape used to draw the selection border.
*/
mxVertexHandler.prototype.createSelectionShape = function(bounds)
{
var shape = new mxRectangleShape(bounds, null, this.getSelectionColor());
shape.strokewidth = this.getSelectionStrokeWidth();
shape.isDashed = this.isSelectionDashed();
return shape;
};
/**
* Function: getSelectionColor
*
* Returns <mxConstants.VERTEX_SELECTION_COLOR>.
*/
mxVertexHandler.prototype.getSelectionColor = function()
{
return mxConstants.VERTEX_SELECTION_COLOR;
};
/**
* Function: getSelectionStrokeWidth
*
* Returns <mxConstants.VERTEX_SELECTION_STROKEWIDTH>.
*/
mxVertexHandler.prototype.getSelectionStrokeWidth = function()
{
return mxConstants.VERTEX_SELECTION_STROKEWIDTH;
};
/**
* Function: isSelectionDashed
*
* Returns <mxConstants.VERTEX_SELECTION_DASHED>.
*/
mxVertexHandler.prototype.isSelectionDashed = function()
{
return mxConstants.VERTEX_SELECTION_DASHED;
};
/**
* Function: createSizer
*
* Creates a sizer handle for the specified cursor and index and returns
* the new <mxRectangleShape> that represents the handle.
*/
mxVertexHandler.prototype.createSizer = function(cursor, index, size, fillColor)
{
size = size || mxConstants.HANDLE_SIZE;
var bounds = new mxRectangle(0, 0, size, size);
var sizer = this.createSizerShape(bounds, index, fillColor);
if (sizer.isHtmlAllowed() && this.state.text != null && this.state.text.node.parentNode == this.graph.container)
{
sizer.bounds.height -= 1;
sizer.bounds.width -= 1;
sizer.dialect = mxConstants.DIALECT_STRICTHTML;
sizer.init(this.graph.container);
}
else
{
sizer.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
sizer.init(this.graph.getView().getOverlayPane());
}
mxEvent.redirectMouseEvents(sizer.node, this.graph, this.state);
if (this.graph.isEnabled())
{
sizer.setCursor(cursor);
}
if (!this.isSizerVisible(index))
{
sizer.visible = false;
}
return sizer;
};
/**
* Function: isSizerVisible
*
* Returns true if the sizer for the given index is visible.
* This returns true for all given indices.
*/
mxVertexHandler.prototype.isSizerVisible = function(index)
{
return true;
};
/**
* Function: createSizerShape
*
* Creates the shape used for the sizer handle for the specified bounds an
* index. Only images and rectangles should be returned if support for HTML
* labels with not foreign objects is required.
*/
mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor)
{
if (this.handleImage != null)
{
bounds = new mxRectangle(bounds.x, bounds.y, this.handleImage.width, this.handleImage.height);
var shape = new mxImageShape(bounds, this.handleImage.src);
// Allows HTML rendering of the images
shape.preserveImageAspect = false;
return shape;
}
else if (index == mxEvent.ROTATION_HANDLE)
{
return new mxEllipse(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
}
else
{
return new mxRectangleShape(bounds, fillColor || mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
}
};
/**
* Function: createBounds
*
* Helper method to create an <mxRectangle> around the given centerpoint
* with a width and height of 2*s or 6, if no s is given.
*/
mxVertexHandler.prototype.moveSizerTo = function(shape, x, y)
{
if (shape != null)
{
shape.bounds.x = Math.floor(x - shape.bounds.width / 2);
shape.bounds.y = Math.floor(y - shape.bounds.height / 2);
// Fixes visible inactive handles in VML
if (shape.node != null && shape.node.style.display != 'none')
{
shape.redraw();
}
}
};
/**
* Function: getHandleForEvent
*
* Returns the index of the handle for the given event. This returns the index
* of the sizer from where the event originated or <mxEvent.LABEL_INDEX>.
*/
mxVertexHandler.prototype.getHandleForEvent = function(me)
{
// Connection highlight may consume events before they reach sizer handle
var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 1;
var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ?
new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null;
function checkShape(shape)
{
return shape != null && (me.isSource(shape) || (hit != null && mxUtils.intersects(shape.bounds, hit) &&
shape.node.style.display != 'none' && shape.node.style.visibility != 'hidden'));
}
if (this.customHandles != null && this.isCustomHandleEvent(me))
{
// Inverse loop order to match display order
for (var i = this.customHandles.length - 1; i >= 0; i--)
{
if (checkShape(this.customHandles[i].shape))
{
// LATER: Return reference to active shape
return mxEvent.CUSTOM_HANDLE - i;
}
}
}
if (checkShape(this.rotationShape))
{
return mxEvent.ROTATION_HANDLE;
}
else if (checkShape(this.labelShape))
{
return mxEvent.LABEL_HANDLE;
}
if (this.sizers != null)
{
for (var i = 0; i < this.sizers.length; i++)
{
if (checkShape(this.sizers[i]))
{
return i;
}
}
}
return null;
};
/**
* Function: isCustomHandleEvent
*
* Returns true if the given event allows custom handles to be changed. This
* implementation returns true.
*/
mxVertexHandler.prototype.isCustomHandleEvent = function(me)
{
return true;
};
/**
* Function: mouseDown
*
* Handles the event if a handle has been clicked. By consuming the
* event all subsequent events of the gesture are redirected to this
* handler.
*/
mxVertexHandler.prototype.mouseDown = function(sender, me)
{
var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 0;
if (!me.isConsumed() && this.graph.isEnabled() && (tol > 0 || me.getState() == this.state))
{
var handle = this.getHandleForEvent(me);
if (handle != null)
{
this.start(me.getGraphX(), me.getGraphY(), handle);
me.consume();
}
}
};
/**
* Function: isLivePreviewBorder
*
* Called if <livePreview> is enabled to check if a border should be painted.
* This implementation returns true if the shape is transparent.
*/
mxVertexHandler.prototype.isLivePreviewBorder = function()
{
return this.state.shape != null && this.state.shape.fill == null && this.state.shape.stroke == null;
};
/**
* Function: start
*
* Starts the handling of the mouse gesture.
*/
mxVertexHandler.prototype.start = function(x, y, index)
{
if (this.selectionBorder != null)
{
this.livePreviewActive = this.livePreview && this.graph.model.getChildCount(this.state.cell) == 0;
this.inTolerance = true;
this.childOffsetX = 0;
this.childOffsetY = 0;
this.index = index;
this.startX = x;
this.startY = y;
// Saves reference to parent state
var model = this.state.view.graph.model;
var parent = model.getParent(this.state.cell);
if (this.state.view.currentRoot != parent && (model.isVertex(parent) || model.isEdge(parent)))
{
this.parentState = this.state.view.graph.view.getState(parent);
}
// Creates a preview that can be on top of any HTML label
this.selectionBorder.node.style.display = (index == mxEvent.ROTATION_HANDLE) ? 'inline' : 'none';
// Creates the border that represents the new bounds
if (!this.livePreviewActive || this.isLivePreviewBorder())
{
this.preview = this.createSelectionShape(this.bounds);
if (!(mxClient.IS_SVG && Number(this.state.style[mxConstants.STYLE_ROTATION] || '0') != 0) &&
this.state.text != null && this.state.text.node.parentNode == this.graph.container)
{
this.preview.dialect = mxConstants.DIALECT_STRICTHTML;
this.preview.init(this.graph.container);
}
else
{
this.preview.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.preview.init(this.graph.view.getOverlayPane());
}
}
// Prepares the handles for live preview
if (this.livePreviewActive)
{
this.hideSizers();
if (index == mxEvent.ROTATION_HANDLE)
{
this.rotationShape.node.style.display = '';
}
else if (index == mxEvent.LABEL_HANDLE)
{
this.labelShape.node.style.display = '';
}
else if (this.sizers != null && this.sizers[index] != null)
{
this.sizers[index].node.style.display = '';
}
else if (index <= mxEvent.CUSTOM_HANDLE && this.customHandles != null)
{
this.customHandles[mxEvent.CUSTOM_HANDLE - index].setVisible(true);
}
// Gets the array of connected edge handlers for redrawing
var edges = this.graph.getEdges(this.state.cell);
this.edgeHandlers = [];
for (var i = 0; i < edges.length; i++)
{
var handler = this.graph.selectionCellsHandler.getHandler(edges[i]);
if (handler != null)
{
this.edgeHandlers.push(handler);
}
}
}
}
};
/**
* Function: hideHandles
*
* Shortcut to <hideSizers>.
*/
mxVertexHandler.prototype.setHandlesVisible = function(visible)
{
if (this.sizers != null)
{
for (var i = 0; i < this.sizers.length; i++)
{
this.sizers[i].node.style.display = (visible) ? '' : 'none';
}
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
this.customHandles[i].setVisible(visible);
}
}
};
/**
* Function: hideSizers
*
* Hides all sizers except.
*
* Starts the handling of the mouse gesture.
*/
mxVertexHandler.prototype.hideSizers = function()
{
this.setHandlesVisible(false);
};
/**
* Function: checkTolerance
*
* Checks if the coordinates for the given event are within the
* <mxGraph.tolerance>. If the event is a mouse event then the tolerance is
* ignored.
*/
mxVertexHandler.prototype.checkTolerance = function(me)
{
if (this.inTolerance && this.startX != null && this.startY != null)
{
if (mxEvent.isMouseEvent(me.getEvent()) ||
Math.abs(me.getGraphX() - this.startX) > this.graph.tolerance ||
Math.abs(me.getGraphY() - this.startY) > this.graph.tolerance)
{
this.inTolerance = false;
}
}
};
/**
* Function: updateHint
*
* Hook for subclassers do show details while the handler is active.
*/
mxVertexHandler.prototype.updateHint = function(me) { };
/**
* Function: removeHint
*
* Hooks for subclassers to hide details when the handler gets inactive.
*/
mxVertexHandler.prototype.removeHint = function() { };
/**
* Function: roundAngle
*
* Hook for rounding the angle. This uses Math.round.
*/
mxVertexHandler.prototype.roundAngle = function(angle)
{
return Math.round(angle * 10) / 10;
};
/**
* Function: roundLength
*
* Hook for rounding the unscaled width or height. This uses Math.round.
*/
mxVertexHandler.prototype.roundLength = function(length)
{
return Math.round(length);
};
/**
* Function: mouseMove
*
* Handles the event by updating the preview.
*/
mxVertexHandler.prototype.mouseMove = function(sender, me)
{
if (!me.isConsumed() && this.index != null)
{
// Checks tolerance for ignoring single clicks
this.checkTolerance(me);
if (!this.inTolerance)
{
if (this.index <= mxEvent.CUSTOM_HANDLE)
{
if (this.customHandles != null)
{
this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me);
this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].active = true;
}
}
else if (this.index == mxEvent.LABEL_HANDLE)
{
this.moveLabel(me);
}
else if (this.index == mxEvent.ROTATION_HANDLE)
{
this.rotateVertex(me);
}
else
{
this.resizeVertex(me);
}
this.updateHint(me);
}
me.consume();
}
// Workaround for disabling the connect highlight when over handle
else if (!this.graph.isMouseDown && this.getHandleForEvent(me) != null)
{
me.consume(false);
}
};
/**
* Function: rotateVertex
*
* Rotates the vertex.
*/
mxVertexHandler.prototype.moveLabel = function(me)
{
var point = new mxPoint(me.getGraphX(), me.getGraphY());
var tr = this.graph.view.translate;
var scale = this.graph.view.scale;
if (this.graph.isGridEnabledEvent(me.getEvent()))
{
point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale;
point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale;
}
var index = (this.rotationShape != null) ? this.sizers.length - 2 : this.sizers.length - 1;
this.moveSizerTo(this.sizers[index], point.x, point.y);
};
/**
* Function: rotateVertex
*
* Rotates the vertex.
*/
mxVertexHandler.prototype.rotateVertex = function(me)
{
var point = new mxPoint(me.getGraphX(), me.getGraphY());
var dx = this.state.x + this.state.width / 2 - point.x;
var dy = this.state.y + this.state.height / 2 - point.y;
this.currentAlpha = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : ((dy < 0) ? 180 : 0);
if (dx > 0)
{
this.currentAlpha -= 180;
}
// Rotation raster
if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent()))
{
var dx = point.x - this.state.getCenterX();
var dy = point.y - this.state.getCenterY();
var dist = Math.abs(Math.sqrt(dx * dx + dy * dy) - 20) * 3;
var raster = Math.max(1, 5 * Math.min(3, Math.max(0, Math.round(80 / Math.abs(dist)))));
this.currentAlpha = Math.round(this.currentAlpha / raster) * raster;
}
else
{
this.currentAlpha = this.roundAngle(this.currentAlpha);
}
this.selectionBorder.rotation = this.currentAlpha;
this.selectionBorder.redraw();
if (this.livePreviewActive)
{
this.redrawHandles();
}
};
/**
* Function: rotateVertex
*
* Rotates the vertex.
*/
mxVertexHandler.prototype.resizeVertex = function(me)
{
var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY());
var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0');
var point = new mxPoint(me.getGraphX(), me.getGraphY());
var tr = this.graph.view.translate;
var scale = this.graph.view.scale;
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var dx = point.x - this.startX;
var dy = point.y - this.startY;
// Rotates vector for mouse gesture
var tx = cos * dx - sin * dy;
var ty = sin * dx + cos * dy;
dx = tx;
dy = ty;
var geo = this.graph.getCellGeometry(this.state.cell);
this.unscaledBounds = this.union(geo, dx / scale, dy / scale, this.index,
this.graph.isGridEnabledEvent(me.getEvent()), 1,
new mxPoint(0, 0), this.isConstrainedEvent(me),
this.isCenteredEvent(this.state, me));
// Keeps vertex within maximum graph or parent bounds
if (!geo.relative)
{
var max = this.graph.getMaximumGraphBounds();
// Handles child cells
if (max != null && this.parentState != null)
{
max = mxRectangle.fromRectangle(max);
max.x -= (this.parentState.x - tr.x * scale) / scale;
max.y -= (this.parentState.y - tr.y * scale) / scale;
}
if (this.graph.isConstrainChild(this.state.cell))
{
var tmp = this.graph.getCellContainmentArea(this.state.cell);
if (tmp != null)
{
var overlap = this.graph.getOverlap(this.state.cell);
if (overlap > 0)
{
tmp = mxRectangle.fromRectangle(tmp);
tmp.x -= tmp.width * overlap;
tmp.y -= tmp.height * overlap;
tmp.width += 2 * tmp.width * overlap;
tmp.height += 2 * tmp.height * overlap;
}
if (max == null)
{
max = tmp;
}
else
{
max = mxRectangle.fromRectangle(max);
max.intersect(tmp);
}
}
}
if (max != null)
{
if (this.unscaledBounds.x < max.x)
{
this.unscaledBounds.width -= max.x - this.unscaledBounds.x;
this.unscaledBounds.x = max.x;
}
if (this.unscaledBounds.y < max.y)
{
this.unscaledBounds.height -= max.y - this.unscaledBounds.y;
this.unscaledBounds.y = max.y;
}
if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width)
{
this.unscaledBounds.width -= this.unscaledBounds.x +
this.unscaledBounds.width - max.x - max.width;
}
if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height)
{
this.unscaledBounds.height -= this.unscaledBounds.y +
this.unscaledBounds.height - max.y - max.height;
}
}
}
this.bounds = new mxRectangle(((this.parentState != null) ? this.parentState.x : tr.x * scale) +
(this.unscaledBounds.x) * scale, ((this.parentState != null) ? this.parentState.y : tr.y * scale) +
(this.unscaledBounds.y) * scale, this.unscaledBounds.width * scale, this.unscaledBounds.height * scale);
if (geo.relative && this.parentState != null)
{
this.bounds.x += this.state.x - this.parentState.x;
this.bounds.y += this.state.y - this.parentState.y;
}
cos = Math.cos(alpha);
sin = Math.sin(alpha);
var c2 = new mxPoint(this.bounds.getCenterX(), this.bounds.getCenterY());
var dx = c2.x - ct.x;
var dy = c2.y - ct.y;
var dx2 = cos * dx - sin * dy;
var dy2 = sin * dx + cos * dy;
var dx3 = dx2 - dx;
var dy3 = dy2 - dy;
var dx4 = this.bounds.x - this.state.x;
var dy4 = this.bounds.y - this.state.y;
var dx5 = cos * dx4 - sin * dy4;
var dy5 = sin * dx4 + cos * dy4;
this.bounds.x += dx3;
this.bounds.y += dy3;
// Rounds unscaled bounds to int
this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale);
this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale);
this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width);
this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height);
// Shifts the children according to parent offset
if (!this.graph.isCellCollapsed(this.state.cell) && (dx3 != 0 || dy3 != 0))
{
this.childOffsetX = this.state.x - this.bounds.x + dx5;
this.childOffsetY = this.state.y - this.bounds.y + dy5;
}
else
{
this.childOffsetX = 0;
this.childOffsetY = 0;
}
if (this.livePreviewActive)
{
this.updateLivePreview(me);
}
if (this.preview != null)
{
this.drawPreview();
}
};
/**
* Function: updateLivePreview
*
* Repaints the live preview.
*/
mxVertexHandler.prototype.updateLivePreview = function(me)
{
// TODO: Apply child offset to children in live preview
var scale = this.graph.view.scale;
var tr = this.graph.view.translate;
// Saves current state
var tempState = this.state.clone();
// Temporarily changes size and origin
this.state.x = this.bounds.x;
this.state.y = this.bounds.y;
this.state.origin = new mxPoint(this.state.x / scale - tr.x, this.state.y / scale - tr.y);
this.state.width = this.bounds.width;
this.state.height = this.bounds.height;
// Needed to force update of text bounds
this.state.unscaledWidth = null;
// Redraws cell and handles
var off = this.state.absoluteOffset;
off = new mxPoint(off.x, off.y);
// Required to store and reset absolute offset for updating label position
this.state.absoluteOffset.x = 0;
this.state.absoluteOffset.y = 0;
var geo = this.graph.getCellGeometry(this.state.cell);
if (geo != null)
{
var offset = geo.offset || this.EMPTY_POINT;
if (offset != null && !geo.relative)
{
this.state.absoluteOffset.x = this.state.view.scale * offset.x;
this.state.absoluteOffset.y = this.state.view.scale * offset.y;
}
this.state.view.updateVertexLabelOffset(this.state);
}
// Draws the live preview
this.state.view.graph.cellRenderer.redraw(this.state, true);
// Redraws connected edges TODO: Include child edges
this.state.view.invalidate(this.state.cell);
this.state.invalid = false;
this.state.view.validate();
this.redrawHandles();
// Hides folding icon
if (this.state.control != null && this.state.control.node != null)
{
this.state.control.node.style.visibility = 'hidden';
}
// Restores current state
this.state.setState(tempState);
};
/**
* Function: mouseUp
*
* Handles the event by applying the changes to the geometry.
*/
mxVertexHandler.prototype.mouseUp = function(sender, me)
{
if (this.index != null && this.state != null)
{
var point = new mxPoint(me.getGraphX(), me.getGraphY());
var index = this.index;
this.index = null;
this.graph.getModel().beginUpdate();
try
{
if (index <= mxEvent.CUSTOM_HANDLE)
{
if (this.customHandles != null)
{
this.customHandles[mxEvent.CUSTOM_HANDLE - index].active = false;
this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute();
}
}
else if (index == mxEvent.ROTATION_HANDLE)
{
if (this.currentAlpha != null)
{
var delta = this.currentAlpha - (this.state.style[mxConstants.STYLE_ROTATION] || 0);
if (delta != 0)
{
this.rotateCell(this.state.cell, delta);
}
}
else
{
this.rotateClick();
}
}
else
{
var gridEnabled = this.graph.isGridEnabledEvent(me.getEvent());
var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0');
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var dx = point.x - this.startX;
var dy = point.y - this.startY;
// Rotates vector for mouse gesture
var tx = cos * dx - sin * dy;
var ty = sin * dx + cos * dy;
dx = tx;
dy = ty;
var s = this.graph.view.scale;
var recurse = this.isRecursiveResize(this.state, me);
this.resizeCell(this.state.cell, this.roundLength(dx / s), this.roundLength(dy / s),
index, gridEnabled, this.isConstrainedEvent(me), recurse);
}
}
finally
{
this.graph.getModel().endUpdate();
}
me.consume();
this.reset();
}
};
/**
* Function: rotateCell
*
* Rotates the given cell to the given rotation.
*/
mxVertexHandler.prototype.isRecursiveResize = function(state, me)
{
return this.graph.isRecursiveResize(this.state);
};
/**
* Function: rotateClick
*
* Hook for subclassers to implement a single click on the rotation handle.
* This code is executed as part of the model transaction. This implementation
* is empty.
*/
mxVertexHandler.prototype.rotateClick = function() { };
/**
* Function: rotateCell
*
* Rotates the given cell and its children by the given angle in degrees.
*
* Parameters:
*
* cell - <mxCell> to be rotated.
* angle - Angle in degrees.
*/
mxVertexHandler.prototype.rotateCell = function(cell, angle, parent)
{
if (angle != 0)
{
var model = this.graph.getModel();
if (model.isVertex(cell) || model.isEdge(cell))
{
if (!model.isEdge(cell))
{
var state = this.graph.view.getState(cell);
var style = (state != null) ? state.style : this.graph.getCellStyle(cell);
if (style != null)
{
var total = (style[mxConstants.STYLE_ROTATION] || 0) + angle;
this.graph.setCellStyles(mxConstants.STYLE_ROTATION, total, [cell]);
}
}
var geo = this.graph.getCellGeometry(cell);
if (geo != null)
{
var pgeo = this.graph.getCellGeometry(parent);
if (pgeo != null && !model.isEdge(parent))
{
geo = geo.clone();
geo.rotate(angle, new mxPoint(pgeo.width / 2, pgeo.height / 2));
model.setGeometry(cell, geo);
}
if ((model.isVertex(cell) && !geo.relative) || model.isEdge(cell))
{
// Recursive rotation
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.rotateCell(model.getChildAt(cell, i), angle, cell);
}
}
}
}
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxVertexHandler.prototype.reset = function()
{
if (this.sizers != null && this.index != null && this.sizers[this.index] != null &&
this.sizers[this.index].node.style.display == 'none')
{
this.sizers[this.index].node.style.display = '';
}
this.currentAlpha = null;
this.inTolerance = null;
this.index = null;
// TODO: Reset and redraw cell states for live preview
if (this.preview != null)
{
this.preview.destroy();
this.preview = null;
}
if (this.livePreviewActive && this.sizers != null)
{
for (var i = 0; i < this.sizers.length; i++)
{
if (this.sizers[i] != null)
{
this.sizers[i].node.style.display = '';
}
}
// Shows folding icon
if (this.state.control != null && this.state.control.node != null)
{
this.state.control.node.style.visibility = '';
}
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
if (this.customHandles[i].active)
{
this.customHandles[i].active = false;
this.customHandles[i].reset();
}
else
{
this.customHandles[i].setVisible(true);
}
}
}
// Checks if handler has been destroyed
if (this.selectionBorder != null)
{
this.selectionBorder.node.style.display = 'inline';
this.selectionBounds = this.getSelectionBounds(this.state);
this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y,
this.selectionBounds.width, this.selectionBounds.height);
this.drawPreview();
}
this.removeHint();
this.redrawHandles();
this.edgeHandlers = null;
this.unscaledBounds = null;
this.livePreviewActive = null;
};
/**
* Function: resizeCell
*
* Uses the given vector to change the bounds of the given cell
* in the graph using <mxGraph.resizeCell>.
*/
mxVertexHandler.prototype.resizeCell = function(cell, dx, dy, index, gridEnabled, constrained, recurse)
{
var geo = this.graph.model.getGeometry(cell);
if (geo != null)
{
if (index == mxEvent.LABEL_HANDLE)
{
var scale = this.graph.view.scale;
dx = Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale);
dy = Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale);
geo = geo.clone();
if (geo.offset == null)
{
geo.offset = new mxPoint(dx, dy);
}
else
{
geo.offset.x += dx;
geo.offset.y += dy;
}
this.graph.model.setGeometry(cell, geo);
}
else if (this.unscaledBounds != null)
{
var scale = this.graph.view.scale;
if (this.childOffsetX != 0 || this.childOffsetY != 0)
{
this.moveChildren(cell, Math.round(this.childOffsetX / scale), Math.round(this.childOffsetY / scale));
}
this.graph.resizeCell(cell, this.unscaledBounds, recurse);
}
}
};
/**
* Function: moveChildren
*
* Moves the children of the given cell by the given vector.
*/
mxVertexHandler.prototype.moveChildren = function(cell, dx, dy)
{
var model = this.graph.getModel();
var childCount = model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
var child = model.getChildAt(cell, i);
var geo = this.graph.getCellGeometry(child);
if (geo != null)
{
geo = geo.clone();
geo.translate(dx, dy);
model.setGeometry(child, geo);
}
}
};
/**
* Function: union
*
* Returns the union of the given bounds and location for the specified
* handle index.
*
* To override this to limit the size of vertex via a minWidth/-Height style,
* the following code can be used.
*
* (code)
* var vertexHandlerUnion = mxVertexHandler.prototype.union;
* mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained)
* {
* var result = vertexHandlerUnion.apply(this, arguments);
*
* result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0));
* result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0));
*
* return result;
* };
* (end)
*
* The minWidth/-Height style can then be used as follows:
*
* (code)
* graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;');
* (end)
*
* To override this to update the height for a wrapped text if the width of a vertex is
* changed, the following can be used.
*
* (code)
* var mxVertexHandlerUnion = mxVertexHandler.prototype.union;
* mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained)
* {
* var result = mxVertexHandlerUnion.apply(this, arguments);
* var s = this.state;
*
* if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) &&
* s.text != null && s.style[mxConstants.STYLE_WHITE_SPACE] == 'wrap')
* {
* var label = this.graph.getLabel(s.cell);
* var fontSize = mxUtils.getNumber(s.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE);
* var ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft
*
* result.height = mxUtils.getSizeForString(label, fontSize, s.style[mxConstants.STYLE_FONTFAMILY], ww).height;
* }
*
* return result;
* };
* (end)
*/
mxVertexHandler.prototype.union = function(bounds, dx, dy, index, gridEnabled, scale, tr, constrained, centered)
{
if (this.singleSizer)
{
var x = bounds.x + bounds.width + dx;
var y = bounds.y + bounds.height + dy;
if (gridEnabled)
{
x = this.graph.snap(x / scale) * scale;
y = this.graph.snap(y / scale) * scale;
}
var rect = new mxRectangle(bounds.x, bounds.y, 0, 0);
rect.add(new mxRectangle(x, y, 0, 0));
return rect;
}
else
{
var w0 = bounds.width;
var h0 = bounds.height;
var left = bounds.x - tr.x * scale;
var right = left + w0;
var top = bounds.y - tr.y * scale;
var bottom = top + h0;
var cx = left + w0 / 2;
var cy = top + h0 / 2;
if (index > 4 /* Bottom Row */)
{
bottom = bottom + dy;
if (gridEnabled)
{
bottom = this.graph.snap(bottom / scale) * scale;
}
}
else if (index < 3 /* Top Row */)
{
top = top + dy;
if (gridEnabled)
{
top = this.graph.snap(top / scale) * scale;
}
}
if (index == 0 || index == 3 || index == 5 /* Left */)
{
left += dx;
if (gridEnabled)
{
left = this.graph.snap(left / scale) * scale;
}
}
else if (index == 2 || index == 4 || index == 7 /* Right */)
{
right += dx;
if (gridEnabled)
{
right = this.graph.snap(right / scale) * scale;
}
}
var width = right - left;
var height = bottom - top;
if (constrained)
{
var geo = this.graph.getCellGeometry(this.state.cell);
if (geo != null)
{
var aspect = geo.width / geo.height;
if (index== 1 || index== 2 || index == 7 || index == 6)
{
width = height * aspect;
}
else
{
height = width / aspect;
}
if (index == 0)
{
left = right - width;
top = bottom - height;
}
}
}
if (centered)
{
width += (width - w0);
height += (height - h0);
var cdx = cx - (left + width / 2);
var cdy = cy - (top + height / 2);
left += cdx;
top += cdy;
right += cdx;
bottom += cdy;
}
// Flips over left side
if (width < 0)
{
left += width;
width = Math.abs(width);
}
// Flips over top side
if (height < 0)
{
top += height;
height = Math.abs(height);
}
var result = new mxRectangle(left + tr.x * scale, top + tr.y * scale, width, height);
if (this.minBounds != null)
{
result.width = Math.max(result.width, this.minBounds.x * scale + this.minBounds.width * scale +
Math.max(0, this.x0 * scale - result.x));
result.height = Math.max(result.height, this.minBounds.y * scale + this.minBounds.height * scale +
Math.max(0, this.y0 * scale - result.y));
}
return result;
}
};
/**
* Function: redraw
*
* Redraws the handles and the preview.
*/
mxVertexHandler.prototype.redraw = function(ignoreHandles)
{
this.selectionBounds = this.getSelectionBounds(this.state);
this.bounds = new mxRectangle(this.selectionBounds.x, this.selectionBounds.y, this.selectionBounds.width, this.selectionBounds.height);
this.drawPreview();
if (!ignoreHandles)
{
this.redrawHandles();
}
};
/**
* Returns the padding to be used for drawing handles for the current <bounds>.
*/
mxVertexHandler.prototype.getHandlePadding = function()
{
// KNOWN: Tolerance depends on event type (eg. 0 for mouse events)
var result = new mxPoint(0, 0);
var tol = this.tolerance;
if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null &&
(this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol ||
this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol))
{
tol /= 2;
result.x = this.sizers[0].bounds.width + tol;
result.y = this.sizers[0].bounds.height + tol;
}
return result;
};
/**
* Function: redrawHandles
*
* Redraws the handles. To hide certain handles the following code can be used.
*
* (code)
* mxVertexHandler.prototype.redrawHandles = function()
* {
* mxVertexHandlerRedrawHandles.apply(this, arguments);
*
* if (this.sizers != null && this.sizers.length > 7)
* {
* this.sizers[1].node.style.display = 'none';
* this.sizers[6].node.style.display = 'none';
* }
* };
* (end)
*/
mxVertexHandler.prototype.redrawHandles = function()
{
var tol = this.tolerance;
this.horizontalOffset = 0;
this.verticalOffset = 0;
var s = this.bounds;
if (this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null)
{
if (this.index == null && this.manageSizers && this.sizers.length >= 8)
{
// KNOWN: Tolerance depends on event type (eg. 0 for mouse events)
var padding = this.getHandlePadding();
this.horizontalOffset = padding.x;
this.verticalOffset = padding.y;
if (this.horizontalOffset != 0 || this.verticalOffset != 0)
{
s = new mxRectangle(s.x, s.y, s.width, s.height);
s.x -= this.horizontalOffset / 2;
s.width += this.horizontalOffset;
s.y -= this.verticalOffset / 2;
s.height += this.verticalOffset;
}
if (this.sizers.length >= 8)
{
if ((s.width < 2 * this.sizers[0].bounds.width + 2 * tol) ||
(s.height < 2 * this.sizers[0].bounds.height + 2 * tol))
{
this.sizers[0].node.style.display = 'none';
this.sizers[2].node.style.display = 'none';
this.sizers[5].node.style.display = 'none';
this.sizers[7].node.style.display = 'none';
}
else
{
this.sizers[0].node.style.display = '';
this.sizers[2].node.style.display = '';
this.sizers[5].node.style.display = '';
this.sizers[7].node.style.display = '';
}
}
}
var r = s.x + s.width;
var b = s.y + s.height;
if (this.singleSizer)
{
this.moveSizerTo(this.sizers[0], r, b);
}
else
{
var cx = s.x + s.width / 2;
var cy = s.y + s.height / 2;
if (this.sizers.length >= 8)
{
var crs = ['nw-resize', 'n-resize', 'ne-resize', 'e-resize', 'se-resize', 's-resize', 'sw-resize', 'w-resize'];
var alpha = mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0');
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var da = Math.round(alpha * 4 / Math.PI);
var ct = new mxPoint(s.getCenterX(), s.getCenterY());
var pt = mxUtils.getRotatedPoint(new mxPoint(s.x, s.y), cos, sin, ct);
this.moveSizerTo(this.sizers[0], pt.x, pt.y);
this.sizers[0].setCursor(crs[mxUtils.mod(0 + da, crs.length)]);
pt.x = cx;
pt.y = s.y;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[1], pt.x, pt.y);
this.sizers[1].setCursor(crs[mxUtils.mod(1 + da, crs.length)]);
pt.x = r;
pt.y = s.y;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[2], pt.x, pt.y);
this.sizers[2].setCursor(crs[mxUtils.mod(2 + da, crs.length)]);
pt.x = s.x;
pt.y = cy;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[3], pt.x, pt.y);
this.sizers[3].setCursor(crs[mxUtils.mod(7 + da, crs.length)]);
pt.x = r;
pt.y = cy;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[4], pt.x, pt.y);
this.sizers[4].setCursor(crs[mxUtils.mod(3 + da, crs.length)]);
pt.x = s.x;
pt.y = b;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[5], pt.x, pt.y);
this.sizers[5].setCursor(crs[mxUtils.mod(6 + da, crs.length)]);
pt.x = cx;
pt.y = b;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[6], pt.x, pt.y);
this.sizers[6].setCursor(crs[mxUtils.mod(5 + da, crs.length)]);
pt.x = r;
pt.y = b;
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
this.moveSizerTo(this.sizers[7], pt.x, pt.y);
this.sizers[7].setCursor(crs[mxUtils.mod(4 + da, crs.length)]);
this.moveSizerTo(this.sizers[8], cx + this.state.absoluteOffset.x, cy + this.state.absoluteOffset.y);
}
else if (this.state.width >= 2 && this.state.height >= 2)
{
this.moveSizerTo(this.sizers[0], cx + this.state.absoluteOffset.x, cy + this.state.absoluteOffset.y);
}
else
{
this.moveSizerTo(this.sizers[0], this.state.x, this.state.y);
}
}
}
if (this.rotationShape != null)
{
var alpha = mxUtils.toRadians((this.currentAlpha != null) ? this.currentAlpha : this.state.style[mxConstants.STYLE_ROTATION] || '0');
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var ct = new mxPoint(this.state.getCenterX(), this.state.getCenterY());
var pt = mxUtils.getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct);
if (this.rotationShape.node != null)
{
this.moveSizerTo(this.rotationShape, pt.x, pt.y);
// Hides rotation handle during text editing
this.rotationShape.node.style.visibility = (this.state.view.graph.isEditing()) ? 'hidden' : '';
}
}
if (this.selectionBorder != null)
{
this.selectionBorder.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0');
}
if (this.edgeHandlers != null)
{
for (var i = 0; i < this.edgeHandlers.length; i++)
{
this.edgeHandlers[i].redraw();
}
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
var temp = this.customHandles[i].shape.node.style.display;
this.customHandles[i].redraw();
this.customHandles[i].shape.node.style.display = temp;
// Hides custom handles during text editing
this.customHandles[i].shape.node.style.visibility = (this.graph.isEditing()) ? 'hidden' : '';
}
}
this.updateParentHighlight();
};
/**
* Function: getRotationHandlePosition
*
* Returns an <mxPoint> that defines the rotation handle position.
*/
mxVertexHandler.prototype.getRotationHandlePosition = function()
{
return new mxPoint(this.bounds.x + this.bounds.width / 2, this.bounds.y + this.rotationHandleVSpacing)
};
/**
* Function: updateParentHighlight
*
* Updates the highlight of the parent if <parentHighlightEnabled> is true.
*/
mxVertexHandler.prototype.updateParentHighlight = function()
{
// If not destroyed
if (this.selectionBorder != null)
{
if (this.parentHighlight != null)
{
var parent = this.graph.model.getParent(this.state.cell);
if (this.graph.model.isVertex(parent))
{
var pstate = this.graph.view.getState(parent);
var b = this.parentHighlight.bounds;
if (pstate != null && (b.x != pstate.x || b.y != pstate.y ||
b.width != pstate.width || b.height != pstate.height))
{
this.parentHighlight.bounds = pstate;
this.parentHighlight.redraw();
}
}
else
{
this.parentHighlight.destroy();
this.parentHighlight = null;
}
}
else if (this.parentHighlightEnabled)
{
var parent = this.graph.model.getParent(this.state.cell);
if (this.graph.model.isVertex(parent))
{
var pstate = this.graph.view.getState(parent);
if (pstate != null)
{
this.parentHighlight = this.createParentHighlightShape(pstate);
// VML dialect required here for event transparency in IE
this.parentHighlight.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.parentHighlight.pointerEvents = false;
this.parentHighlight.rotation = Number(pstate.style[mxConstants.STYLE_ROTATION] || '0');
this.parentHighlight.init(this.graph.getView().getOverlayPane());
}
}
}
}
};
/**
* Function: drawPreview
*
* Redraws the preview.
*/
mxVertexHandler.prototype.drawPreview = function()
{
if (this.preview != null)
{
this.preview.bounds = this.bounds;
if (this.preview.node.parentNode == this.graph.container)
{
this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1);
this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1);
}
this.preview.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0');
this.preview.redraw();
}
this.selectionBorder.bounds = this.bounds;
this.selectionBorder.redraw();
if (this.parentHighlight != null)
{
this.parentHighlight.redraw();
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxVertexHandler.prototype.destroy = function()
{
if (this.escapeHandler != null)
{
this.state.view.graph.removeListener(this.escapeHandler);
this.escapeHandler = null;
}
if (this.preview != null)
{
this.preview.destroy();
this.preview = null;
}
if (this.parentHighlight != null)
{
this.parentHighlight.destroy();
this.parentHighlight = null;
}
if (this.selectionBorder != null)
{
this.selectionBorder.destroy();
this.selectionBorder = null;
}
this.labelShape = null;
this.removeHint();
if (this.sizers != null)
{
for (var i = 0; i < this.sizers.length; i++)
{
this.sizers[i].destroy();
}
this.sizers = null;
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
this.customHandles[i].destroy();
}
this.customHandles = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxEdgeHandler
*
* Graph event handler that reconnects edges and modifies control points and
* the edge label location. Uses <mxTerminalMarker> for finding and
* highlighting new source and target vertices. This handler is automatically
* created in <mxGraph.createHandler> for each selected edge.
*
* To enable adding/removing control points, the following code can be used:
*
* (code)
* mxEdgeHandler.prototype.addEnabled = true;
* mxEdgeHandler.prototype.removeEnabled = true;
* (end)
*
* Note: This experimental feature is not recommended for production use.
*
* Constructor: mxEdgeHandler
*
* Constructs an edge handler for the specified <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> of the cell to be handled.
*/
function mxEdgeHandler(state)
{
if (state != null)
{
this.state = state;
this.init();
// Handles escape keystrokes
this.escapeHandler = mxUtils.bind(this, function(sender, evt)
{
var dirty = this.index != null;
this.reset();
if (dirty)
{
this.graph.cellRenderer.redraw(this.state, false, state.view.isRendering());
}
});
this.state.view.graph.addListener(mxEvent.ESCAPE, this.escapeHandler);
}
};
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxEdgeHandler.prototype.graph = null;
/**
* Variable: state
*
* Reference to the <mxCellState> being modified.
*/
mxEdgeHandler.prototype.state = null;
/**
* Variable: marker
*
* Holds the <mxTerminalMarker> which is used for highlighting terminals.
*/
mxEdgeHandler.prototype.marker = null;
/**
* Variable: constraintHandler
*
* Holds the <mxConstraintHandler> used for drawing and highlighting
* constraints.
*/
mxEdgeHandler.prototype.constraintHandler = null;
/**
* Variable: error
*
* Holds the current validation error while a connection is being changed.
*/
mxEdgeHandler.prototype.error = null;
/**
* Variable: shape
*
* Holds the <mxShape> that represents the preview edge.
*/
mxEdgeHandler.prototype.shape = null;
/**
* Variable: bends
*
* Holds the <mxShapes> that represent the points.
*/
mxEdgeHandler.prototype.bends = null;
/**
* Variable: labelShape
*
* Holds the <mxShape> that represents the label position.
*/
mxEdgeHandler.prototype.labelShape = null;
/**
* Variable: cloneEnabled
*
* Specifies if cloning by control-drag is enabled. Default is true.
*/
mxEdgeHandler.prototype.cloneEnabled = true;
/**
* Variable: addEnabled
*
* Specifies if adding bends by shift-click is enabled. Default is false.
* Note: This experimental feature is not recommended for production use.
*/
mxEdgeHandler.prototype.addEnabled = false;
/**
* Variable: removeEnabled
*
* Specifies if removing bends by shift-click is enabled. Default is false.
* Note: This experimental feature is not recommended for production use.
*/
mxEdgeHandler.prototype.removeEnabled = false;
/**
* Variable: dblClickRemoveEnabled
*
* Specifies if removing bends by double click is enabled. Default is false.
*/
mxEdgeHandler.prototype.dblClickRemoveEnabled = false;
/**
* Variable: mergeRemoveEnabled
*
* Specifies if removing bends by dropping them on other bends is enabled.
* Default is false.
*/
mxEdgeHandler.prototype.mergeRemoveEnabled = false;
/**
* Variable: straightRemoveEnabled
*
* Specifies if removing bends by creating straight segments should be enabled.
* If enabled, this can be overridden by holding down the alt key while moving.
* Default is false.
*/
mxEdgeHandler.prototype.straightRemoveEnabled = false;
/**
* Variable: virtualBendsEnabled
*
* Specifies if virtual bends should be added in the center of each
* segments. These bends can then be used to add new waypoints.
* Default is false.
*/
mxEdgeHandler.prototype.virtualBendsEnabled = false;
/**
* Variable: virtualBendOpacity
*
* Opacity to be used for virtual bends (see <virtualBendsEnabled>).
* Default is 20.
*/
mxEdgeHandler.prototype.virtualBendOpacity = 20;
/**
* Variable: parentHighlightEnabled
*
* Specifies if the parent should be highlighted if a child cell is selected.
* Default is false.
*/
mxEdgeHandler.prototype.parentHighlightEnabled = false;
/**
* Variable: preferHtml
*
* Specifies if bends should be added to the graph container. This is updated
* in <init> based on whether the edge or one of its terminals has an HTML
* label in the container.
*/
mxEdgeHandler.prototype.preferHtml = false;
/**
* Variable: allowHandleBoundsCheck
*
* Specifies if the bounds of handles should be used for hit-detection in IE
* Default is true.
*/
mxEdgeHandler.prototype.allowHandleBoundsCheck = true;
/**
* Variable: snapToTerminals
*
* Specifies if waypoints should snap to the routing centers of terminals.
* Default is false.
*/
mxEdgeHandler.prototype.snapToTerminals = false;
/**
* Variable: handleImage
*
* Optional <mxImage> to be used as handles. Default is null.
*/
mxEdgeHandler.prototype.handleImage = null;
/**
* Variable: tolerance
*
* Optional tolerance for hit-detection in <getHandleForEvent>. Default is 0.
*/
mxEdgeHandler.prototype.tolerance = 0;
/**
* Variable: outlineConnect
*
* Specifies if connections to the outline of a highlighted target should be
* enabled. This will allow to place the connection point along the outline of
* the highlighted target. Default is false.
*/
mxEdgeHandler.prototype.outlineConnect = false;
/**
* Variable: manageLabelHandle
*
* Specifies if the label handle should be moved if it intersects with another
* handle. Uses <checkLabelHandle> for checking and moving. Default is false.
*/
mxEdgeHandler.prototype.manageLabelHandle = false;
/**
* Function: init
*
* Initializes the shapes required for this edge handler.
*/
mxEdgeHandler.prototype.init = function()
{
this.graph = this.state.view.graph;
this.marker = this.createMarker();
this.constraintHandler = new mxConstraintHandler(this.graph);
// Clones the original points from the cell
// and makes sure at least one point exists
this.points = [];
// Uses the absolute points of the state
// for the initial configuration and preview
this.abspoints = this.getSelectionPoints(this.state);
this.shape = this.createSelectionShape(this.abspoints);
this.shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
this.shape.init(this.graph.getView().getOverlayPane());
this.shape.pointerEvents = false;
this.shape.setCursor(mxConstants.CURSOR_MOVABLE_EDGE);
mxEvent.redirectMouseEvents(this.shape.node, this.graph, this.state);
// Updates preferHtml
this.preferHtml = this.state.text != null &&
this.state.text.node.parentNode == this.graph.container;
if (!this.preferHtml)
{
// Checks source terminal
var sourceState = this.state.getVisibleTerminalState(true);
if (sourceState != null)
{
this.preferHtml = sourceState.text != null &&
sourceState.text.node.parentNode == this.graph.container;
}
if (!this.preferHtml)
{
// Checks target terminal
var targetState = this.state.getVisibleTerminalState(false);
if (targetState != null)
{
this.preferHtml = targetState.text != null &&
targetState.text.node.parentNode == this.graph.container;
}
}
}
// Adds highlight for parent group
if (this.parentHighlightEnabled)
{
var parent = this.graph.model.getParent(this.state.cell);
if (this.graph.model.isVertex(parent))
{
var pstate = this.graph.view.getState(parent);
if (pstate != null)
{
this.parentHighlight = this.createParentHighlightShape(pstate);
// VML dialect required here for event transparency in IE
this.parentHighlight.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
this.parentHighlight.pointerEvents = false;
this.parentHighlight.rotation = Number(pstate.style[mxConstants.STYLE_ROTATION] || '0');
this.parentHighlight.init(this.graph.getView().getOverlayPane());
}
}
}
// Creates bends for the non-routed absolute points
// or bends that don't correspond to points
if (this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells ||
mxGraphHandler.prototype.maxCells <= 0)
{
this.bends = this.createBends();
if (this.isVirtualBendsEnabled())
{
this.virtualBends = this.createVirtualBends();
}
}
// Adds a rectangular handle for the label position
this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y);
this.labelShape = this.createLabelHandleShape();
this.initBend(this.labelShape);
this.labelShape.setCursor(mxConstants.CURSOR_LABEL_HANDLE);
this.customHandles = this.createCustomHandles();
this.redraw();
};
/**
* Function: createCustomHandles
*
* Returns an array of custom handles. This implementation returns null.
*/
mxEdgeHandler.prototype.createCustomHandles = function()
{
return null;
};
/**
* Function: isVirtualBendsEnabled
*
* Returns true if virtual bends should be added. This returns true if
* <virtualBendsEnabled> is true and the current style allows and
* renders custom waypoints.
*/
mxEdgeHandler.prototype.isVirtualBendsEnabled = function(evt)
{
return this.virtualBendsEnabled && (this.state.style[mxConstants.STYLE_EDGE] == null ||
this.state.style[mxConstants.STYLE_EDGE] == mxConstants.NONE ||
this.state.style[mxConstants.STYLE_NOEDGESTYLE] == 1) &&
mxUtils.getValue(this.state.style, mxConstants.STYLE_SHAPE, null) != 'arrow';
};
/**
* Function: isAddPointEvent
*
* Returns true if the given event is a trigger to add a new point. This
* implementation returns true if shift is pressed.
*/
mxEdgeHandler.prototype.isAddPointEvent = function(evt)
{
return mxEvent.isShiftDown(evt);
};
/**
* Function: isRemovePointEvent
*
* Returns true if the given event is a trigger to remove a point. This
* implementation returns true if shift is pressed.
*/
mxEdgeHandler.prototype.isRemovePointEvent = function(evt)
{
return mxEvent.isShiftDown(evt);
};
/**
* Function: getSelectionPoints
*
* Returns the list of points that defines the selection stroke.
*/
mxEdgeHandler.prototype.getSelectionPoints = function(state)
{
return state.absolutePoints;
};
/**
* Function: createSelectionShape
*
* Creates the shape used to draw the selection border.
*/
mxEdgeHandler.prototype.createParentHighlightShape = function(bounds)
{
var shape = new mxRectangleShape(bounds, null, this.getSelectionColor());
shape.strokewidth = this.getSelectionStrokeWidth();
shape.isDashed = this.isSelectionDashed();
return shape;
};
/**
* Function: createSelectionShape
*
* Creates the shape used to draw the selection border.
*/
mxEdgeHandler.prototype.createSelectionShape = function(points)
{
var shape = new this.state.shape.constructor();
shape.outline = true;
shape.apply(this.state);
shape.isDashed = this.isSelectionDashed();
shape.stroke = this.getSelectionColor();
shape.isShadow = false;
return shape;
};
/**
* Function: getSelectionColor
*
* Returns <mxConstants.EDGE_SELECTION_COLOR>.
*/
mxEdgeHandler.prototype.getSelectionColor = function()
{
return mxConstants.EDGE_SELECTION_COLOR;
};
/**
* Function: getSelectionStrokeWidth
*
* Returns <mxConstants.EDGE_SELECTION_STROKEWIDTH>.
*/
mxEdgeHandler.prototype.getSelectionStrokeWidth = function()
{
return mxConstants.EDGE_SELECTION_STROKEWIDTH;
};
/**
* Function: isSelectionDashed
*
* Returns <mxConstants.EDGE_SELECTION_DASHED>.
*/
mxEdgeHandler.prototype.isSelectionDashed = function()
{
return mxConstants.EDGE_SELECTION_DASHED;
};
/**
* Function: isConnectableCell
*
* Returns true if the given cell is connectable. This is a hook to
* disable floating connections. This implementation returns true.
*/
mxEdgeHandler.prototype.isConnectableCell = function(cell)
{
return true;
};
/**
* Function: getCellAt
*
* Creates and returns the <mxCellMarker> used in <marker>.
*/
mxEdgeHandler.prototype.getCellAt = function(x, y)
{
return (!this.outlineConnect) ? this.graph.getCellAt(x, y) : null;
};
/**
* Function: createMarker
*
* Creates and returns the <mxCellMarker> used in <marker>.
*/
mxEdgeHandler.prototype.createMarker = function()
{
var marker = new mxCellMarker(this.graph);
var self = this; // closure
// Only returns edges if they are connectable and never returns
// the edge that is currently being modified
marker.getCell = function(me)
{
var cell = mxCellMarker.prototype.getCell.apply(this, arguments);
// Checks for cell at preview point (with grid)
if ((cell == self.state.cell || cell == null) && self.currentPoint != null)
{
cell = self.graph.getCellAt(self.currentPoint.x, self.currentPoint.y);
}
// Uses connectable parent vertex if one exists
if (cell != null && !this.graph.isCellConnectable(cell))
{
var parent = this.graph.getModel().getParent(cell);
if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
{
cell = parent;
}
}
var model = self.graph.getModel();
if ((this.graph.isSwimlane(cell) && self.currentPoint != null &&
this.graph.hitsSwimlaneContent(cell, self.currentPoint.x, self.currentPoint.y)) ||
(!self.isConnectableCell(cell)) || (cell == self.state.cell ||
(cell != null && !self.graph.connectableEdges && model.isEdge(cell))) ||
model.isAncestor(self.state.cell, cell))
{
cell = null;
}
if (!this.graph.isCellConnectable(cell))
{
cell = null;
}
return cell;
};
// Sets the highlight color according to validateConnection
marker.isValidState = function(state)
{
var model = self.graph.getModel();
var other = self.graph.view.getTerminalPort(state,
self.graph.view.getState(model.getTerminal(self.state.cell,
!self.isSource)), !self.isSource);
var otherCell = (other != null) ? other.cell : null;
var source = (self.isSource) ? state.cell : otherCell;
var target = (self.isSource) ? otherCell : state.cell;
// Updates the error message of the handler
self.error = self.validateConnection(source, target);
return self.error == null;
};
return marker;
};
/**
* Function: validateConnection
*
* Returns the error message or an empty string if the connection for the
* given source, target pair is not valid. Otherwise it returns null. This
* implementation uses <mxGraph.getEdgeValidationError>.
*
* Parameters:
*
* source - <mxCell> that represents the source terminal.
* target - <mxCell> that represents the target terminal.
*/
mxEdgeHandler.prototype.validateConnection = function(source, target)
{
return this.graph.getEdgeValidationError(this.state.cell, source, target);
};
/**
* Function: createBends
*
* Creates and returns the bends used for modifying the edge. This is
* typically an array of <mxRectangleShapes>.
*/
mxEdgeHandler.prototype.createBends = function()
{
var cell = this.state.cell;
var bends = [];
for (var i = 0; i < this.abspoints.length; i++)
{
if (this.isHandleVisible(i))
{
var source = i == 0;
var target = i == this.abspoints.length - 1;
var terminal = source || target;
if (terminal || this.graph.isCellBendable(cell))
{
(mxUtils.bind(this, function(index)
{
var bend = this.createHandleShape(index);
this.initBend(bend, mxUtils.bind(this, mxUtils.bind(this, function()
{
if (this.dblClickRemoveEnabled)
{
this.removePoint(this.state, index);
}
})));
if (this.isHandleEnabled(i))
{
bend.setCursor((terminal) ? mxConstants.CURSOR_TERMINAL_HANDLE : mxConstants.CURSOR_BEND_HANDLE);
}
bends.push(bend);
if (!terminal)
{
this.points.push(new mxPoint(0,0));
bend.node.style.visibility = 'hidden';
}
}))(i);
}
}
}
return bends;
};
/**
* Function: createVirtualBends
*
* Creates and returns the bends used for modifying the edge. This is
* typically an array of <mxRectangleShapes>.
*/
mxEdgeHandler.prototype.createVirtualBends = function()
{
var cell = this.state.cell;
var last = this.abspoints[0];
var bends = [];
if (this.graph.isCellBendable(cell))
{
for (var i = 1; i < this.abspoints.length; i++)
{
(mxUtils.bind(this, function(bend)
{
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_VIRTUAL_BEND_HANDLE);
bends.push(bend);
}))(this.createHandleShape());
}
}
return bends;
};
/**
* Function: isHandleEnabled
*
* Creates the shape used to display the given bend.
*/
mxEdgeHandler.prototype.isHandleEnabled = function(index)
{
return true;
};
/**
* Function: isHandleVisible
*
* Returns true if the handle at the given index is visible.
*/
mxEdgeHandler.prototype.isHandleVisible = function(index)
{
var source = this.state.getVisibleTerminalState(true);
var target = this.state.getVisibleTerminalState(false);
var geo = this.graph.getCellGeometry(this.state.cell);
var edgeStyle = (geo != null) ? this.graph.view.getEdgeStyle(this.state, geo.points, source, target) : null;
return edgeStyle != mxEdgeStyle.EntityRelation || index == 0 || index == this.abspoints.length - 1;
};
/**
* Function: createHandleShape
*
* Creates the shape used to display the given bend. Note that the index may be
* null for special cases, such as when called from
* <mxElbowEdgeHandler.createVirtualBend>. Only images and rectangles should be
* returned if support for HTML labels with not foreign objects is required.
* Index if null for virtual handles.
*/
mxEdgeHandler.prototype.createHandleShape = function(index)
{
if (this.handleImage != null)
{
var shape = new mxImageShape(new mxRectangle(0, 0, this.handleImage.width, this.handleImage.height), this.handleImage.src);
// Allows HTML rendering of the images
shape.preserveImageAspect = false;
return shape;
}
else
{
var s = mxConstants.HANDLE_SIZE;
if (this.preferHtml)
{
s -= 1;
}
return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
}
};
/**
* Function: createLabelHandleShape
*
* Creates the shape used to display the the label handle.
*/
mxEdgeHandler.prototype.createLabelHandleShape = function()
{
if (this.labelHandleImage != null)
{
var shape = new mxImageShape(new mxRectangle(0, 0, this.labelHandleImage.width, this.labelHandleImage.height), this.labelHandleImage.src);
// Allows HTML rendering of the images
shape.preserveImageAspect = false;
return shape;
}
else
{
var s = mxConstants.LABEL_HANDLE_SIZE;
return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.LABEL_HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR);
}
};
/**
* Function: initBend
*
* Helper method to initialize the given bend.
*
* Parameters:
*
* bend - <mxShape> that represents the bend to be initialized.
*/
mxEdgeHandler.prototype.initBend = function(bend, dblClick)
{
if (this.preferHtml)
{
bend.dialect = mxConstants.DIALECT_STRICTHTML;
bend.init(this.graph.container);
}
else
{
bend.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
bend.init(this.graph.getView().getOverlayPane());
}
mxEvent.redirectMouseEvents(bend.node, this.graph, this.state,
null, null, null, dblClick);
// Fixes lost event tracking for images in quirks / IE8 standards
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
mxEvent.addListener(bend.node, 'dragstart', function(evt)
{
mxEvent.consume(evt);
return false;
});
}
if (mxClient.IS_TOUCH)
{
bend.node.setAttribute('pointer-events', 'none');
}
};
/**
* Function: getHandleForEvent
*
* Returns the index of the handle for the given event.
*/
mxEdgeHandler.prototype.getHandleForEvent = function(me)
{
// Connection highlight may consume events before they reach sizer handle
var tol = (!mxEvent.isMouseEvent(me.getEvent())) ? this.tolerance : 1;
var hit = (this.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)) ?
new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) : null;
var minDistSq = null;
var result = null;
function checkShape(shape)
{
if (shape != null && shape.node.style.display != 'none' && shape.node.style.visibility != 'hidden' &&
(me.isSource(shape) || (hit != null && mxUtils.intersects(shape.bounds, hit))))
{
var dx = me.getGraphX() - shape.bounds.getCenterX();
var dy = me.getGraphY() - shape.bounds.getCenterY();
var tmp = dx * dx + dy * dy;
if (minDistSq == null || tmp <= minDistSq)
{
minDistSq = tmp;
return true;
}
}
return false;
}
if (this.customHandles != null && this.isCustomHandleEvent(me))
{
// Inverse loop order to match display order
for (var i = this.customHandles.length - 1; i >= 0; i--)
{
if (checkShape(this.customHandles[i].shape))
{
// LATER: Return reference to active shape
return mxEvent.CUSTOM_HANDLE - i;
}
}
}
if (me.isSource(this.state.text) || checkShape(this.labelShape))
{
result = mxEvent.LABEL_HANDLE;
}
if (this.bends != null)
{
for (var i = 0; i < this.bends.length; i++)
{
if (checkShape(this.bends[i]))
{
result = i;
}
}
}
if (this.virtualBends != null && this.isAddVirtualBendEvent(me))
{
for (var i = 0; i < this.virtualBends.length; i++)
{
if (checkShape(this.virtualBends[i]))
{
result = mxEvent.VIRTUAL_HANDLE - i;
}
}
}
return result;
};
/**
* Function: isAddVirtualBendEvent
*
* Returns true if the given event allows virtual bends to be added. This
* implementation returns true.
*/
mxEdgeHandler.prototype.isAddVirtualBendEvent = function(me)
{
return true;
};
/**
* Function: isCustomHandleEvent
*
* Returns true if the given event allows custom handles to be changed. This
* implementation returns true.
*/
mxEdgeHandler.prototype.isCustomHandleEvent = function(me)
{
return true;
};
/**
* Function: mouseDown
*
* Handles the event by checking if a special element of the handler
* was clicked, in which case the index parameter is non-null. The
* indices may be one of <LABEL_HANDLE> or the number of the respective
* control point. The source and target points are used for reconnecting
* the edge.
*/
mxEdgeHandler.prototype.mouseDown = function(sender, me)
{
var handle = this.getHandleForEvent(me);
if (this.bends != null && this.bends[handle] != null)
{
var b = this.bends[handle].bounds;
this.snapPoint = new mxPoint(b.getCenterX(), b.getCenterY());
}
if (this.addEnabled && handle == null && this.isAddPointEvent(me.getEvent()))
{
this.addPoint(this.state, me.getEvent());
me.consume();
}
else if (handle != null && !me.isConsumed() && this.graph.isEnabled())
{
if (this.removeEnabled && this.isRemovePointEvent(me.getEvent()))
{
this.removePoint(this.state, handle);
}
else if (handle != mxEvent.LABEL_HANDLE || this.graph.isLabelMovable(me.getCell()))
{
if (handle <= mxEvent.VIRTUAL_HANDLE)
{
mxUtils.setOpacity(this.virtualBends[mxEvent.VIRTUAL_HANDLE - handle].node, 100);
}
this.start(me.getX(), me.getY(), handle);
}
me.consume();
}
};
/**
* Function: start
*
* Starts the handling of the mouse gesture.
*/
mxEdgeHandler.prototype.start = function(x, y, index)
{
this.startX = x;
this.startY = y;
this.isSource = (this.bends == null) ? false : index == 0;
this.isTarget = (this.bends == null) ? false : index == this.bends.length - 1;
this.isLabel = index == mxEvent.LABEL_HANDLE;
if (this.isSource || this.isTarget)
{
var cell = this.state.cell;
var terminal = this.graph.model.getTerminal(cell, this.isSource);
if ((terminal == null && this.graph.isTerminalPointMovable(cell, this.isSource)) ||
(terminal != null && this.graph.isCellDisconnectable(cell, terminal, this.isSource)))
{
this.index = index;
}
}
else
{
this.index = index;
}
// Hides other custom handles
if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE)
{
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
if (i != mxEvent.CUSTOM_HANDLE - this.index)
{
this.customHandles[i].setVisible(false);
}
}
}
}
};
/**
* Function: clonePreviewState
*
* Returns a clone of the current preview state for the given point and terminal.
*/
mxEdgeHandler.prototype.clonePreviewState = function(point, terminal)
{
return this.state.clone();
};
/**
* Function: getSnapToTerminalTolerance
*
* Returns the tolerance for the guides. Default value is
* gridSize * scale / 2.
*/
mxEdgeHandler.prototype.getSnapToTerminalTolerance = function()
{
return this.graph.gridSize * this.graph.view.scale / 2;
};
/**
* Function: updateHint
*
* Hook for subclassers do show details while the handler is active.
*/
mxEdgeHandler.prototype.updateHint = function(me, point) { };
/**
* Function: removeHint
*
* Hooks for subclassers to hide details when the handler gets inactive.
*/
mxEdgeHandler.prototype.removeHint = function() { };
/**
* Function: roundLength
*
* Hook for rounding the unscaled width or height. This uses Math.round.
*/
mxEdgeHandler.prototype.roundLength = function(length)
{
return Math.round(length);
};
/**
* Function: isSnapToTerminalsEvent
*
* Returns true if <snapToTerminals> is true and if alt is not pressed.
*/
mxEdgeHandler.prototype.isSnapToTerminalsEvent = function(me)
{
return this.snapToTerminals && !mxEvent.isAltDown(me.getEvent());
};
/**
* Function: getPointForEvent
*
* Returns the point for the given event.
*/
mxEdgeHandler.prototype.getPointForEvent = function(me)
{
var view = this.graph.getView();
var scale = view.scale;
var point = new mxPoint(this.roundLength(me.getGraphX() / scale) * scale,
this.roundLength(me.getGraphY() / scale) * scale);
var tt = this.getSnapToTerminalTolerance();
var overrideX = false;
var overrideY = false;
if (tt > 0 && this.isSnapToTerminalsEvent(me))
{
function snapToPoint(pt)
{
if (pt != null)
{
var x = pt.x;
if (Math.abs(point.x - x) < tt)
{
point.x = x;
overrideX = true;
}
var y = pt.y;
if (Math.abs(point.y - y) < tt)
{
point.y = y;
overrideY = true;
}
}
}
// Temporary function
function snapToTerminal(terminal)
{
if (terminal != null)
{
snapToPoint.call(this, new mxPoint(view.getRoutingCenterX(terminal),
view.getRoutingCenterY(terminal)));
}
};
snapToTerminal.call(this, this.state.getVisibleTerminalState(true));
snapToTerminal.call(this, this.state.getVisibleTerminalState(false));
if (this.state.absolutePoints != null)
{
for (var i = 0; i < this.state.absolutePoints.length; i++)
{
snapToPoint.call(this, this.state.absolutePoints[i]);
}
}
}
if (this.graph.isGridEnabledEvent(me.getEvent()))
{
var tr = view.translate;
if (!overrideX)
{
point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale;
}
if (!overrideY)
{
point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale;
}
}
return point;
};
/**
* Function: getPreviewTerminalState
*
* Updates the given preview state taking into account the state of the constraint handler.
*/
mxEdgeHandler.prototype.getPreviewTerminalState = function(me)
{
this.constraintHandler.update(me, this.isSource, true, me.isSource(this.marker.highlight.shape) ? null : this.currentPoint);
if (this.constraintHandler.currentFocus != null && this.constraintHandler.currentConstraint != null)
{
// Handles special case where grid is large and connection point is at actual point in which
// case the outline is not followed as long as we're < gridSize / 2 away from that point
if (this.marker.highlight != null && this.marker.highlight.state != null &&
this.marker.highlight.state.cell == this.constraintHandler.currentFocus.cell)
{
// Direct repaint needed if cell already highlighted
if (this.marker.highlight.shape.stroke != 'transparent')
{
this.marker.highlight.shape.stroke = 'transparent';
this.marker.highlight.repaint();
}
}
else
{
this.marker.markCell(this.constraintHandler.currentFocus.cell, 'transparent');
}
var model = this.graph.getModel();
var other = this.graph.view.getTerminalPort(this.state,
this.graph.view.getState(model.getTerminal(this.state.cell,
!this.isSource)), !this.isSource);
var otherCell = (other != null) ? other.cell : null;
var source = (this.isSource) ? this.constraintHandler.currentFocus.cell : otherCell;
var target = (this.isSource) ? otherCell : this.constraintHandler.currentFocus.cell;
// Updates the error message of the handler
this.error = this.validateConnection(source, target);
var result = null;
if (this.error == null)
{
result = this.constraintHandler.currentFocus;
}
else
{
this.constraintHandler.reset();
}
return result;
}
else if (!this.graph.isIgnoreTerminalEvent(me.getEvent()))
{
this.marker.process(me);
var state = this.marker.getValidState();
if (state != null && this.graph.isCellLocked(state.cell))
{
this.marker.reset();
}
return this.marker.getValidState();
}
else
{
this.marker.reset();
return null;
}
};
/**
* Function: getPreviewPoints
*
* Updates the given preview state taking into account the state of the constraint handler.
*
* Parameters:
*
* pt - <mxPoint> that contains the current pointer position.
* me - Optional <mxMouseEvent> that contains the current event.
*/
mxEdgeHandler.prototype.getPreviewPoints = function(pt, me)
{
var geometry = this.graph.getCellGeometry(this.state.cell);
var points = (geometry.points != null) ? geometry.points.slice() : null;
var point = new mxPoint(pt.x, pt.y);
var result = null;
if (!this.isSource && !this.isTarget)
{
this.convertPoint(point, false);
if (points == null)
{
points = [point];
}
else
{
// Adds point from virtual bend
if (this.index <= mxEvent.VIRTUAL_HANDLE)
{
points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 0, point);
}
// Removes point if dragged on terminal point
if (!this.isSource && !this.isTarget)
{
for (var i = 0; i < this.bends.length; i++)
{
if (i != this.index)
{
var bend = this.bends[i];
if (bend != null && mxUtils.contains(bend.bounds, pt.x, pt.y))
{
if (this.index <= mxEvent.VIRTUAL_HANDLE)
{
points.splice(mxEvent.VIRTUAL_HANDLE - this.index, 1);
}
else
{
points.splice(this.index - 1, 1);
}
result = points;
}
}
}
// Removes point if user tries to straighten a segment
if (result == null && this.straightRemoveEnabled && (me == null || !mxEvent.isAltDown(me.getEvent())))
{
var tol = this.graph.tolerance * this.graph.tolerance;
var abs = this.state.absolutePoints.slice();
abs[this.index] = pt;
// Handes special case where removing waypoint affects tolerance (flickering)
var src = this.state.getVisibleTerminalState(true);
if (src != null)
{
var c = this.graph.getConnectionConstraint(this.state, src, true);
// Checks if point is not fixed
if (c == null || this.graph.getConnectionPoint(src, c) == null)
{
abs[0] = new mxPoint(src.view.getRoutingCenterX(src), src.view.getRoutingCenterY(src));
}
}
var trg = this.state.getVisibleTerminalState(false);
if (trg != null)
{
var c = this.graph.getConnectionConstraint(this.state, trg, false);
// Checks if point is not fixed
if (c == null || this.graph.getConnectionPoint(trg, c) == null)
{
abs[abs.length - 1] = new mxPoint(trg.view.getRoutingCenterX(trg), trg.view.getRoutingCenterY(trg));
}
}
function checkRemove(idx, tmp)
{
if (idx > 0 && idx < abs.length - 1 &&
mxUtils.ptSegDistSq(abs[idx - 1].x, abs[idx - 1].y,
abs[idx + 1].x, abs[idx + 1].y, tmp.x, tmp.y) < tol)
{
points.splice(idx - 1, 1);
result = points;
}
};
// LATER: Check if other points can be removed if a segment is made straight
checkRemove(this.index, pt);
}
}
// Updates existing point
if (result == null && this.index > mxEvent.VIRTUAL_HANDLE)
{
points[this.index - 1] = point;
}
}
}
else if (this.graph.resetEdgesOnConnect)
{
points = null;
}
return (result != null) ? result : points;
};
/**
* Function: isOutlineConnectEvent
*
* Returns true if <outlineConnect> is true and the source of the event is the outline shape
* or shift is pressed.
*/
mxEdgeHandler.prototype.isOutlineConnectEvent = function(me)
{
var offset = mxUtils.getOffset(this.graph.container);
var evt = me.getEvent();
var clientX = mxEvent.getClientX(evt);
var clientY = mxEvent.getClientY(evt);
var doc = document.documentElement;
var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
var gridX = this.currentPoint.x - this.graph.container.scrollLeft + offset.x - left;
var gridY = this.currentPoint.y - this.graph.container.scrollTop + offset.y - top;
return this.outlineConnect && !mxEvent.isShiftDown(me.getEvent()) &&
(me.isSource(this.marker.highlight.shape) ||
(mxEvent.isAltDown(me.getEvent()) && me.getState() != null) ||
this.marker.highlight.isHighlightAt(clientX, clientY) ||
((gridX != clientX || gridY != clientY) && me.getState() == null &&
this.marker.highlight.isHighlightAt(gridX, gridY)));
};
/**
* Function: updatePreviewState
*
* Updates the given preview state taking into account the state of the constraint handler.
*/
mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState, me, outline)
{
// Computes the points for the edge style and terminals
var sourceState = (this.isSource) ? terminalState : this.state.getVisibleTerminalState(true);
var targetState = (this.isTarget) ? terminalState : this.state.getVisibleTerminalState(false);
var sourceConstraint = this.graph.getConnectionConstraint(edge, sourceState, true);
var targetConstraint = this.graph.getConnectionConstraint(edge, targetState, false);
var constraint = this.constraintHandler.currentConstraint;
if (constraint == null && outline)
{
if (terminalState != null)
{
// Handles special case where mouse is on outline away from actual end point
// in which case the grid is ignored and mouse point is used instead
if (me.isSource(this.marker.highlight.shape))
{
point = new mxPoint(me.getGraphX(), me.getGraphY());
}
constraint = this.graph.getOutlineConstraint(point, terminalState, me);
this.constraintHandler.setFocus(me, terminalState, this.isSource);
this.constraintHandler.currentConstraint = constraint;
this.constraintHandler.currentPoint = point;
}
else
{
constraint = new mxConnectionConstraint();
}
}
if (this.outlineConnect && this.marker.highlight != null && this.marker.highlight.shape != null)
{
var s = this.graph.view.scale;
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null)
{
this.marker.highlight.shape.stroke = (outline) ? mxConstants.OUTLINE_HIGHLIGHT_COLOR : 'transparent';
this.marker.highlight.shape.strokewidth = mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH / s / s;
this.marker.highlight.repaint();
}
else if (this.marker.hasValidState())
{
this.marker.highlight.shape.stroke = (this.marker.getValidState() == me.getState()) ?
mxConstants.DEFAULT_VALID_COLOR : 'transparent';
this.marker.highlight.shape.strokewidth = mxConstants.HIGHLIGHT_STROKEWIDTH / s / s;
this.marker.highlight.repaint();
}
}
if (this.isSource)
{
sourceConstraint = constraint;
}
else if (this.isTarget)
{
targetConstraint = constraint;
}
if (this.isSource || this.isTarget)
{
if (constraint != null && constraint.point != null)
{
edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X] = constraint.point.x;
edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y] = constraint.point.y;
}
else
{
delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_X : mxConstants.STYLE_ENTRY_X];
delete edge.style[(this.isSource) ? mxConstants.STYLE_EXIT_Y : mxConstants.STYLE_ENTRY_Y];
}
}
edge.setVisibleTerminalState(sourceState, true);
edge.setVisibleTerminalState(targetState, false);
if (!this.isSource || sourceState != null)
{
edge.view.updateFixedTerminalPoint(edge, sourceState, true, sourceConstraint);
}
if (!this.isTarget || targetState != null)
{
edge.view.updateFixedTerminalPoint(edge, targetState, false, targetConstraint);
}
if ((this.isSource || this.isTarget) && terminalState == null)
{
edge.setAbsoluteTerminalPoint(point, this.isSource);
if (this.marker.getMarkedState() == null)
{
this.error = (this.graph.allowDanglingEdges) ? null : '';
}
}
edge.view.updatePoints(edge, this.points, sourceState, targetState);
edge.view.updateFloatingTerminalPoints(edge, sourceState, targetState);
};
/**
* Function: mouseMove
*
* Handles the event by updating the preview.
*/
mxEdgeHandler.prototype.mouseMove = function(sender, me)
{
if (this.index != null && this.marker != null)
{
this.currentPoint = this.getPointForEvent(me);
this.error = null;
// Uses the current point from the constraint handler if available
if (!this.graph.isIgnoreTerminalEvent(me.getEvent()) && mxEvent.isShiftDown(me.getEvent()) && this.snapPoint != null)
{
if (Math.abs(this.snapPoint.x - this.currentPoint.x) < Math.abs(this.snapPoint.y - this.currentPoint.y))
{
this.currentPoint.x = this.snapPoint.x;
}
else
{
this.currentPoint.y = this.snapPoint.y;
}
}
if (this.index <= mxEvent.CUSTOM_HANDLE && this.index > mxEvent.VIRTUAL_HANDLE)
{
if (this.customHandles != null)
{
this.customHandles[mxEvent.CUSTOM_HANDLE - this.index].processEvent(me);
}
}
else if (this.isLabel)
{
this.label.x = this.currentPoint.x;
this.label.y = this.currentPoint.y;
}
else
{
this.points = this.getPreviewPoints(this.currentPoint, me);
var terminalState = (this.isSource || this.isTarget) ? this.getPreviewTerminalState(me) : null;
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null &&
this.constraintHandler.currentPoint != null)
{
this.currentPoint = this.constraintHandler.currentPoint.clone();
}
else if (this.outlineConnect)
{
// Need to check outline before cloning terminal state
var outline = (this.isSource || this.isTarget) ? this.isOutlineConnectEvent(me) : false
if (outline)
{
terminalState = this.marker.highlight.state;
}
else if (terminalState != null && terminalState != me.getState() && this.marker.highlight.shape != null)
{
this.marker.highlight.shape.stroke = 'transparent';
this.marker.highlight.repaint();
terminalState = null;
}
}
if (terminalState != null && this.graph.isCellLocked(terminalState.cell))
{
terminalState = null;
this.marker.reset();
}
var clone = this.clonePreviewState(this.currentPoint, (terminalState != null) ? terminalState.cell : null);
this.updatePreviewState(clone, this.currentPoint, terminalState, me, outline);
// Sets the color of the preview to valid or invalid, updates the
// points of the preview and redraws
var color = (this.error == null) ? this.marker.validColor : this.marker.invalidColor;
this.setPreviewColor(color);
this.abspoints = clone.absolutePoints;
this.active = true;
}
// This should go before calling isOutlineConnectEvent above. As a workaround
// we add an offset of gridSize to the hint to avoid problem with hit detection
// in highlight.isHighlightAt (which uses comonentFromPoint)
this.updateHint(me, this.currentPoint);
this.drawPreview();
mxEvent.consume(me.getEvent());
me.consume();
}
// Workaround for disabling the connect highlight when over handle
else if (mxClient.IS_IE && this.getHandleForEvent(me) != null)
{
me.consume(false);
}
};
/**
* Function: mouseUp
*
* Handles the event to applying the previewed changes on the edge by
* using <moveLabel>, <connect> or <changePoints>.
*/
mxEdgeHandler.prototype.mouseUp = function(sender, me)
{
// Workaround for wrong event source in Webkit
if (this.index != null && this.marker != null)
{
var edge = this.state.cell;
var index = this.index;
this.index = null;
// Ignores event if mouse has not been moved
if (me.getX() != this.startX || me.getY() != this.startY)
{
var clone = !this.graph.isIgnoreTerminalEvent(me.getEvent()) && this.graph.isCloneEvent(me.getEvent()) &&
this.cloneEnabled && this.graph.isCellsCloneable();
// Displays the reason for not carriying out the change
// if there is an error message with non-zero length
if (this.error != null)
{
if (this.error.length > 0)
{
this.graph.validationAlert(this.error);
}
}
else if (index <= mxEvent.CUSTOM_HANDLE && index > mxEvent.VIRTUAL_HANDLE)
{
if (this.customHandles != null)
{
var model = this.graph.getModel();
model.beginUpdate();
try
{
this.customHandles[mxEvent.CUSTOM_HANDLE - index].execute();
}
finally
{
model.endUpdate();
}
}
}
else if (this.isLabel)
{
this.moveLabel(this.state, this.label.x, this.label.y);
}
else if (this.isSource || this.isTarget)
{
var terminal = null;
if (this.constraintHandler.currentConstraint != null &&
this.constraintHandler.currentFocus != null)
{
terminal = this.constraintHandler.currentFocus.cell;
}
if (terminal == null && this.marker.hasValidState() && this.marker.highlight != null &&
this.marker.highlight.shape != null &&
this.marker.highlight.shape.stroke != 'transparent' &&
this.marker.highlight.shape.stroke != 'white')
{
terminal = this.marker.validState.cell;
}
if (terminal != null)
{
var model = this.graph.getModel();
var parent = model.getParent(edge);
model.beginUpdate();
try
{
// Clones and adds the cell
if (clone)
{
var geo = model.getGeometry(edge);
var clone = this.graph.cloneCell(edge);
model.add(parent, clone, model.getChildCount(parent));
if (geo != null)
{
geo = geo.clone();
model.setGeometry(clone, geo);
}
var other = model.getTerminal(edge, !this.isSource);
this.graph.connectCell(clone, other, !this.isSource);
edge = clone;
}
edge = this.connect(edge, terminal, this.isSource, clone, me);
}
finally
{
model.endUpdate();
}
}
else if (this.graph.isAllowDanglingEdges())
{
var pt = this.abspoints[(this.isSource) ? 0 : this.abspoints.length - 1];
pt.x = this.roundLength(pt.x / this.graph.view.scale - this.graph.view.translate.x);
pt.y = this.roundLength(pt.y / this.graph.view.scale - this.graph.view.translate.y);
var pstate = this.graph.getView().getState(
this.graph.getModel().getParent(edge));
if (pstate != null)
{
pt.x -= pstate.origin.x;
pt.y -= pstate.origin.y;
}
pt.x -= this.graph.panDx / this.graph.view.scale;
pt.y -= this.graph.panDy / this.graph.view.scale;
// Destroys and recreates this handler
edge = this.changeTerminalPoint(edge, pt, this.isSource, clone);
}
}
else if (this.active)
{
edge = this.changePoints(edge, this.points, clone);
}
else
{
this.graph.getView().invalidate(this.state.cell);
this.graph.getView().validate(this.state.cell);
}
}
// Resets the preview color the state of the handler if this
// handler has not been recreated
if (this.marker != null)
{
this.reset();
// Updates the selection if the edge has been cloned
if (edge != this.state.cell)
{
this.graph.setSelectionCell(edge);
}
}
me.consume();
}
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxEdgeHandler.prototype.reset = function()
{
if (this.active)
{
this.refresh();
}
this.error = null;
this.index = null;
this.label = null;
this.points = null;
this.snapPoint = null;
this.isLabel = false;
this.isSource = false;
this.isTarget = false;
this.active = false;
if (this.livePreview && this.sizers != null)
{
for (var i = 0; i < this.sizers.length; i++)
{
if (this.sizers[i] != null)
{
this.sizers[i].node.style.display = '';
}
}
}
if (this.marker != null)
{
this.marker.reset();
}
if (this.constraintHandler != null)
{
this.constraintHandler.reset();
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
this.customHandles[i].reset();
}
}
this.setPreviewColor(mxConstants.EDGE_SELECTION_COLOR);
this.removeHint();
this.redraw();
};
/**
* Function: setPreviewColor
*
* Sets the color of the preview to the given value.
*/
mxEdgeHandler.prototype.setPreviewColor = function(color)
{
if (this.shape != null)
{
this.shape.stroke = color;
}
};
/**
* Function: convertPoint
*
* Converts the given point in-place from screen to unscaled, untranslated
* graph coordinates and applies the grid. Returns the given, modified
* point instance.
*
* Parameters:
*
* point - <mxPoint> to be converted.
* gridEnabled - Boolean that specifies if the grid should be applied.
*/
mxEdgeHandler.prototype.convertPoint = function(point, gridEnabled)
{
var scale = this.graph.getView().getScale();
var tr = this.graph.getView().getTranslate();
if (gridEnabled)
{
point.x = this.graph.snap(point.x);
point.y = this.graph.snap(point.y);
}
point.x = Math.round(point.x / scale - tr.x);
point.y = Math.round(point.y / scale - tr.y);
var pstate = this.graph.getView().getState(
this.graph.getModel().getParent(this.state.cell));
if (pstate != null)
{
point.x -= pstate.origin.x;
point.y -= pstate.origin.y;
}
return point;
};
/**
* Function: moveLabel
*
* Changes the coordinates for the label of the given edge.
*
* Parameters:
*
* edge - <mxCell> that represents the edge.
* x - Integer that specifies the x-coordinate of the new location.
* y - Integer that specifies the y-coordinate of the new location.
*/
mxEdgeHandler.prototype.moveLabel = function(edgeState, x, y)
{
var model = this.graph.getModel();
var geometry = model.getGeometry(edgeState.cell);
if (geometry != null)
{
var scale = this.graph.getView().scale;
geometry = geometry.clone();
if (geometry.relative)
{
// Resets the relative location stored inside the geometry
var pt = this.graph.getView().getRelativePoint(edgeState, x, y);
geometry.x = Math.round(pt.x * 10000) / 10000;
geometry.y = Math.round(pt.y);
// Resets the offset inside the geometry to find the offset
// from the resulting point
geometry.offset = new mxPoint(0, 0);
var pt = this.graph.view.getPoint(edgeState, geometry);
geometry.offset = new mxPoint(Math.round((x - pt.x) / scale), Math.round((y - pt.y) / scale));
}
else
{
var points = edgeState.absolutePoints;
var p0 = points[0];
var pe = points[points.length - 1];
if (p0 != null && pe != null)
{
var cx = p0.x + (pe.x - p0.x) / 2;
var cy = p0.y + (pe.y - p0.y) / 2;
geometry.offset = new mxPoint(Math.round((x - cx) / scale), Math.round((y - cy) / scale));
geometry.x = 0;
geometry.y = 0;
}
}
model.setGeometry(edgeState.cell, geometry);
}
};
/**
* Function: connect
*
* Changes the terminal or terminal point of the given edge in the graph
* model.
*
* Parameters:
*
* edge - <mxCell> that represents the edge to be reconnected.
* terminal - <mxCell> that represents the new terminal.
* isSource - Boolean indicating if the new terminal is the source or
* target terminal.
* isClone - Boolean indicating if the new connection should be a clone of
* the old edge.
* me - <mxMouseEvent> that contains the mouse up event.
*/
mxEdgeHandler.prototype.connect = function(edge, terminal, isSource, isClone, me)
{
var model = this.graph.getModel();
var parent = model.getParent(edge);
model.beginUpdate();
try
{
var constraint = this.constraintHandler.currentConstraint;
if (constraint == null)
{
constraint = new mxConnectionConstraint();
}
this.graph.connectCell(edge, terminal, isSource, constraint);
}
finally
{
model.endUpdate();
}
return edge;
};
/**
* Function: changeTerminalPoint
*
* Changes the terminal point of the given edge.
*/
mxEdgeHandler.prototype.changeTerminalPoint = function(edge, point, isSource, clone)
{
var model = this.graph.getModel();
model.beginUpdate();
try
{
if (clone)
{
var parent = model.getParent(edge);
var terminal = model.getTerminal(edge, !isSource);
edge = this.graph.cloneCell(edge);
model.add(parent, edge, model.getChildCount(parent));
model.setTerminal(edge, terminal, !isSource);
}
var geo = model.getGeometry(edge);
if (geo != null)
{
geo = geo.clone();
geo.setTerminalPoint(point, isSource);
model.setGeometry(edge, geo);
this.graph.connectCell(edge, null, isSource, new mxConnectionConstraint());
}
}
finally
{
model.endUpdate();
}
return edge;
};
/**
* Function: changePoints
*
* Changes the control points of the given edge in the graph model.
*/
mxEdgeHandler.prototype.changePoints = function(edge, points, clone)
{
var model = this.graph.getModel();
model.beginUpdate();
try
{
if (clone)
{
var parent = model.getParent(edge);
var source = model.getTerminal(edge, true);
var target = model.getTerminal(edge, false);
edge = this.graph.cloneCell(edge);
model.add(parent, edge, model.getChildCount(parent));
model.setTerminal(edge, source, true);
model.setTerminal(edge, target, false);
}
var geo = model.getGeometry(edge);
if (geo != null)
{
geo = geo.clone();
geo.points = points;
model.setGeometry(edge, geo);
}
}
finally
{
model.endUpdate();
}
return edge;
};
/**
* Function: addPoint
*
* Adds a control point for the given state and event.
*/
mxEdgeHandler.prototype.addPoint = function(state, evt)
{
var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt),
mxEvent.getClientY(evt));
var gridEnabled = this.graph.isGridEnabledEvent(evt);
this.convertPoint(pt, gridEnabled);
this.addPointAt(state, pt.x, pt.y);
mxEvent.consume(evt);
};
/**
* Function: addPointAt
*
* Adds a control point at the given point.
*/
mxEdgeHandler.prototype.addPointAt = function(state, x, y)
{
var geo = this.graph.getCellGeometry(state.cell);
var pt = new mxPoint(x, y);
if (geo != null)
{
geo = geo.clone();
var t = this.graph.view.translate;
var s = this.graph.view.scale;
var offset = new mxPoint(t.x * s, t.y * s);
var parent = this.graph.model.getParent(this.state.cell);
if (this.graph.model.isVertex(parent))
{
var pState = this.graph.view.getState(parent);
offset = new mxPoint(pState.x, pState.y);
}
var index = mxUtils.findNearestSegment(state, pt.x * s + offset.x, pt.y * s + offset.y);
if (geo.points == null)
{
geo.points = [pt];
}
else
{
geo.points.splice(index, 0, pt);
}
this.graph.getModel().setGeometry(state.cell, geo);
this.refresh();
this.redraw();
}
};
/**
* Function: removePoint
*
* Removes the control point at the given index from the given state.
*/
mxEdgeHandler.prototype.removePoint = function(state, index)
{
if (index > 0 && index < this.abspoints.length - 1)
{
var geo = this.graph.getCellGeometry(this.state.cell);
if (geo != null && geo.points != null)
{
geo = geo.clone();
geo.points.splice(index - 1, 1);
this.graph.getModel().setGeometry(state.cell, geo);
this.refresh();
this.redraw();
}
}
};
/**
* Function: getHandleFillColor
*
* Returns the fillcolor for the handle at the given index.
*/
mxEdgeHandler.prototype.getHandleFillColor = function(index)
{
var isSource = index == 0;
var cell = this.state.cell;
var terminal = this.graph.getModel().getTerminal(cell, isSource);
var color = mxConstants.HANDLE_FILLCOLOR;
if ((terminal != null && !this.graph.isCellDisconnectable(cell, terminal, isSource)) ||
(terminal == null && !this.graph.isTerminalPointMovable(cell, isSource)))
{
color = mxConstants.LOCKED_HANDLE_FILLCOLOR;
}
else if (terminal != null && this.graph.isCellDisconnectable(cell, terminal, isSource))
{
color = mxConstants.CONNECT_HANDLE_FILLCOLOR;
}
return color;
};
/**
* Function: redraw
*
* Redraws the preview, and the bends- and label control points.
*/
mxEdgeHandler.prototype.redraw = function(ignoreHandles)
{
this.abspoints = this.state.absolutePoints.slice();
var g = this.graph.getModel().getGeometry(this.state.cell);
var pts = g.points;
if (this.bends != null && this.bends.length > 0)
{
if (pts != null)
{
if (this.points == null)
{
this.points = [];
}
for (var i = 1; i < this.bends.length - 1; i++)
{
if (this.bends[i] != null && this.abspoints[i] != null)
{
this.points[i - 1] = pts[i - 1];
}
}
}
}
this.drawPreview();
if (!ignoreHandles)
{
this.redrawHandles();
}
};
/**
* Function: redrawHandles
*
* Redraws the handles.
*/
mxEdgeHandler.prototype.redrawHandles = function()
{
var cell = this.state.cell;
// Updates the handle for the label position
var b = this.labelShape.bounds;
this.label = new mxPoint(this.state.absoluteOffset.x, this.state.absoluteOffset.y);
this.labelShape.bounds = new mxRectangle(Math.round(this.label.x - b.width / 2),
Math.round(this.label.y - b.height / 2), b.width, b.height);
// Shows or hides the label handle depending on the label
var lab = this.graph.getLabel(cell);
this.labelShape.visible = (lab != null && lab.length > 0 && this.graph.isLabelMovable(cell));
if (this.bends != null && this.bends.length > 0)
{
var n = this.abspoints.length - 1;
var p0 = this.abspoints[0];
var x0 = p0.x;
var y0 = p0.y;
b = this.bends[0].bounds;
this.bends[0].bounds = new mxRectangle(Math.floor(x0 - b.width / 2),
Math.floor(y0 - b.height / 2), b.width, b.height);
this.bends[0].fill = this.getHandleFillColor(0);
this.bends[0].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[0].bounds);
}
var pe = this.abspoints[n];
var xn = pe.x;
var yn = pe.y;
var bn = this.bends.length - 1;
b = this.bends[bn].bounds;
this.bends[bn].bounds = new mxRectangle(Math.floor(xn - b.width / 2),
Math.floor(yn - b.height / 2), b.width, b.height);
this.bends[bn].fill = this.getHandleFillColor(bn);
this.bends[bn].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[bn].bounds);
}
this.redrawInnerBends(p0, pe);
}
if (this.abspoints != null && this.virtualBends != null && this.virtualBends.length > 0)
{
var last = this.abspoints[0];
for (var i = 0; i < this.virtualBends.length; i++)
{
if (this.virtualBends[i] != null && this.abspoints[i + 1] != null)
{
var pt = this.abspoints[i + 1];
var b = this.virtualBends[i];
var x = last.x + (pt.x - last.x) / 2;
var y = last.y + (pt.y - last.y) / 2;
b.bounds = new mxRectangle(Math.floor(x - b.bounds.width / 2),
Math.floor(y - b.bounds.height / 2), b.bounds.width, b.bounds.height);
b.redraw();
mxUtils.setOpacity(b.node, this.virtualBendOpacity);
last = pt;
if (this.manageLabelHandle)
{
this.checkLabelHandle(b.bounds);
}
}
}
}
if (this.labelShape != null)
{
this.labelShape.redraw();
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
this.customHandles[i].redraw();
}
}
};
/**
* Function: hideHandles
*
* Shortcut to <hideSizers>.
*/
mxEdgeHandler.prototype.setHandlesVisible = function(visible)
{
if (this.bends != null)
{
for (var i = 0; i < this.bends.length; i++)
{
this.bends[i].node.style.display = (visible) ? '' : 'none';
}
}
if (this.virtualBends != null)
{
for (var i = 0; i < this.virtualBends.length; i++)
{
this.virtualBends[i].node.style.display = (visible) ? '' : 'none';
}
}
if (this.labelShape != null)
{
this.labelShape.node.style.display = (visible) ? '' : 'none';
}
if (this.customHandles != null)
{
for (var i = 0; i < this.customHandles.length; i++)
{
this.customHandles[i].setVisible(visible);
}
}
};
/**
* Function: redrawInnerBends
*
* Updates and redraws the inner bends.
*
* Parameters:
*
* p0 - <mxPoint> that represents the location of the first point.
* pe - <mxPoint> that represents the location of the last point.
*/
mxEdgeHandler.prototype.redrawInnerBends = function(p0, pe)
{
for (var i = 1; i < this.bends.length - 1; i++)
{
if (this.bends[i] != null)
{
if (this.abspoints[i] != null)
{
var x = this.abspoints[i].x;
var y = this.abspoints[i].y;
var b = this.bends[i].bounds;
this.bends[i].node.style.visibility = 'visible';
this.bends[i].bounds = new mxRectangle(Math.round(x - b.width / 2),
Math.round(y - b.height / 2), b.width, b.height);
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[i].bounds);
}
else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(this.bends[i].bounds, this.labelShape.bounds))
{
w = mxConstants.HANDLE_SIZE + 3;
h = mxConstants.HANDLE_SIZE + 3;
this.bends[i].bounds = new mxRectangle(Math.round(x - w / 2), Math.round(y - h / 2), w, h);
}
this.bends[i].redraw();
}
else
{
this.bends[i].destroy();
this.bends[i] = null;
}
}
}
};
/**
* Function: checkLabelHandle
*
* Checks if the label handle intersects the given bounds and moves it if it
* intersects.
*/
mxEdgeHandler.prototype.checkLabelHandle = function(b)
{
if (this.labelShape != null)
{
var b2 = this.labelShape.bounds;
if (mxUtils.intersects(b, b2))
{
if (b.getCenterY() < b2.getCenterY())
{
b2.y = b.y + b.height;
}
else
{
b2.y = b.y - b2.height;
}
}
}
};
/**
* Function: drawPreview
*
* Redraws the preview.
*/
mxEdgeHandler.prototype.drawPreview = function()
{
if (this.isLabel)
{
var b = this.labelShape.bounds;
var bounds = new mxRectangle(Math.round(this.label.x - b.width / 2),
Math.round(this.label.y - b.height / 2), b.width, b.height);
this.labelShape.bounds = bounds;
this.labelShape.redraw();
}
else if (this.shape != null)
{
this.shape.apply(this.state);
this.shape.points = this.abspoints;
this.shape.scale = this.state.view.scale;
this.shape.isDashed = this.isSelectionDashed();
this.shape.stroke = this.getSelectionColor();
this.shape.strokewidth = this.getSelectionStrokeWidth() / this.shape.scale / this.shape.scale;
this.shape.isShadow = false;
this.shape.redraw();
}
if (this.parentHighlight != null)
{
this.parentHighlight.redraw();
}
};
/**
* Function: refresh
*
* Refreshes the bends of this handler.
*/
mxEdgeHandler.prototype.refresh = function()
{
this.abspoints = this.getSelectionPoints(this.state);
this.points = [];
if (this.shape != null)
{
this.shape.points = this.abspoints;
}
if (this.bends != null)
{
this.destroyBends(this.bends);
this.bends = this.createBends();
}
if (this.virtualBends != null)
{
this.destroyBends(this.virtualBends);
this.virtualBends = this.createVirtualBends();
}
if (this.customHandles != null)
{
this.destroyBends(this.customHandles);
this.customHandles = this.createCustomHandles();
}
// Puts label node on top of bends
if (this.labelShape != null && this.labelShape.node != null && this.labelShape.node.parentNode != null)
{
this.labelShape.node.parentNode.appendChild(this.labelShape.node);
}
};
/**
* Function: destroyBends
*
* Destroys all elements in <bends>.
*/
mxEdgeHandler.prototype.destroyBends = function(bends)
{
if (bends != null)
{
for (var i = 0; i < bends.length; i++)
{
if (bends[i] != null)
{
bends[i].destroy();
}
}
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes. This does
* normally not need to be called as handlers are destroyed automatically
* when the corresponding cell is deselected.
*/
mxEdgeHandler.prototype.destroy = function()
{
if (this.escapeHandler != null)
{
this.state.view.graph.removeListener(this.escapeHandler);
this.escapeHandler = null;
}
if (this.marker != null)
{
this.marker.destroy();
this.marker = null;
}
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
if (this.parentHighlight != null)
{
this.parentHighlight.destroy();
this.parentHighlight = null;
}
if (this.labelShape != null)
{
this.labelShape.destroy();
this.labelShape = null;
}
if (this.constraintHandler != null)
{
this.constraintHandler.destroy();
this.constraintHandler = null;
}
this.destroyBends(this.virtualBends);
this.virtualBends = null;
this.destroyBends(this.customHandles);
this.customHandles = null;
this.destroyBends(this.bends);
this.bends = null;
this.removeHint();
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxElbowEdgeHandler
*
* Graph event handler that reconnects edges and modifies control points and
* the edge label location. Uses <mxTerminalMarker> for finding and
* highlighting new source and target vertices. This handler is automatically
* created in <mxGraph.createHandler>. It extends <mxEdgeHandler>.
*
* Constructor: mxEdgeHandler
*
* Constructs an edge handler for the specified <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> of the cell to be modified.
*/
function mxElbowEdgeHandler(state)
{
mxEdgeHandler.call(this, state);
};
/**
* Extends mxEdgeHandler.
*/
mxUtils.extend(mxElbowEdgeHandler, mxEdgeHandler);
/**
* Specifies if a double click on the middle handle should call
* <mxGraph.flipEdge>. Default is true.
*/
mxElbowEdgeHandler.prototype.flipEnabled = true;
/**
* Variable: doubleClickOrientationResource
*
* Specifies the resource key for the tooltip to be displayed on the single
* control point for routed edges. If the resource for this key does not
* exist then the value is used as the error message. Default is
* 'doubleClickOrientation'.
*/
mxElbowEdgeHandler.prototype.doubleClickOrientationResource =
(mxClient.language != 'none') ? 'doubleClickOrientation' : '';
/**
* Function: createBends
*
* Overrides <mxEdgeHandler.createBends> to create custom bends.
*/
mxElbowEdgeHandler.prototype.createBends = function()
{
var bends = [];
// Source
var bend = this.createHandleShape(0);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
// Virtual
bends.push(this.createVirtualBend(mxUtils.bind(this, function(evt)
{
if (!mxEvent.isConsumed(evt) && this.flipEnabled)
{
this.graph.flipEdge(this.state.cell, evt);
mxEvent.consume(evt);
}
})));
this.points.push(new mxPoint(0,0));
// Target
bend = this.createHandleShape(2);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
return bends;
};
/**
* Function: createVirtualBend
*
* Creates a virtual bend that supports double clicking and calls
* <mxGraph.flipEdge>.
*/
mxElbowEdgeHandler.prototype.createVirtualBend = function(dblClickHandler)
{
var bend = this.createHandleShape();
this.initBend(bend, dblClickHandler);
bend.setCursor(this.getCursorForBend());
if (!this.graph.isCellBendable(this.state.cell))
{
bend.node.style.display = 'none';
}
return bend;
};
/**
* Function: getCursorForBend
*
* Returns the cursor to be used for the bend.
*/
mxElbowEdgeHandler.prototype.getCursorForBend = function()
{
return (this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.TopToBottom ||
this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_TOPTOBOTTOM ||
((this.state.style[mxConstants.STYLE_EDGE] == mxEdgeStyle.ElbowConnector ||
this.state.style[mxConstants.STYLE_EDGE] == mxConstants.EDGESTYLE_ELBOW)&&
this.state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) ?
'row-resize' : 'col-resize';
};
/**
* Function: getTooltipForNode
*
* Returns the tooltip for the given node.
*/
mxElbowEdgeHandler.prototype.getTooltipForNode = function(node)
{
var tip = null;
if (this.bends != null && this.bends[1] != null && (node == this.bends[1].node ||
node.parentNode == this.bends[1].node))
{
tip = this.doubleClickOrientationResource;
tip = mxResources.get(tip) || tip; // translate
}
return tip;
};
/**
* Function: convertPoint
*
* Converts the given point in-place from screen to unscaled, untranslated
* graph coordinates and applies the grid.
*
* Parameters:
*
* point - <mxPoint> to be converted.
* gridEnabled - Boolean that specifies if the grid should be applied.
*/
mxElbowEdgeHandler.prototype.convertPoint = function(point, gridEnabled)
{
var scale = this.graph.getView().getScale();
var tr = this.graph.getView().getTranslate();
var origin = this.state.origin;
if (gridEnabled)
{
point.x = this.graph.snap(point.x);
point.y = this.graph.snap(point.y);
}
point.x = Math.round(point.x / scale - tr.x - origin.x);
point.y = Math.round(point.y / scale - tr.y - origin.y);
return point;
};
/**
* Function: redrawInnerBends
*
* Updates and redraws the inner bends.
*
* Parameters:
*
* p0 - <mxPoint> that represents the location of the first point.
* pe - <mxPoint> that represents the location of the last point.
*/
mxElbowEdgeHandler.prototype.redrawInnerBends = function(p0, pe)
{
var g = this.graph.getModel().getGeometry(this.state.cell);
var pts = this.state.absolutePoints;
var pt = null;
// Keeps the virtual bend on the edge shape
if (pts.length > 1)
{
p0 = pts[1];
pe = pts[pts.length - 2];
}
else if (g.points != null && g.points.length > 0)
{
pt = pts[0];
}
if (pt == null)
{
pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
}
else
{
pt = new mxPoint(this.graph.getView().scale * (pt.x + this.graph.getView().translate.x + this.state.origin.x),
this.graph.getView().scale * (pt.y + this.graph.getView().translate.y + this.state.origin.y));
}
// Makes handle slightly bigger if the yellow label handle
// exists and intersects this green handle
var b = this.bends[1].bounds;
var w = b.width;
var h = b.height;
var bounds = new mxRectangle(Math.round(pt.x - w / 2), Math.round(pt.y - h / 2), w, h);
if (this.manageLabelHandle)
{
this.checkLabelHandle(bounds);
}
else if (this.handleImage == null && this.labelShape.visible && mxUtils.intersects(bounds, this.labelShape.bounds))
{
w = mxConstants.HANDLE_SIZE + 3;
h = mxConstants.HANDLE_SIZE + 3;
bounds = new mxRectangle(Math.floor(pt.x - w / 2), Math.floor(pt.y - h / 2), w, h);
}
this.bends[1].bounds = bounds;
this.bends[1].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[1].bounds);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
function mxEdgeSegmentHandler(state)
{
mxEdgeHandler.call(this, state);
};
/**
* Extends mxEdgeHandler.
*/
mxUtils.extend(mxEdgeSegmentHandler, mxElbowEdgeHandler);
/**
* Function: getCurrentPoints
*
* Returns the current absolute points.
*/
mxEdgeSegmentHandler.prototype.getCurrentPoints = function()
{
var pts = this.state.absolutePoints;
if (pts != null)
{
// Special case for straight edges where we add a virtual middle handle for moving the edge
var tol = Math.max(1, this.graph.view.scale);
if (pts.length == 2 || (pts.length == 3 &&
(Math.abs(pts[0].x - pts[1].x) < tol && Math.abs(pts[1].x - pts[2].x) < tol ||
Math.abs(pts[0].y - pts[1].y) < tol && Math.abs(pts[1].y - pts[2].y) < tol)))
{
var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2;
var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2;
pts = [pts[0], new mxPoint(cx, cy), new mxPoint(cx, cy), pts[pts.length - 1]];
}
}
return pts;
};
/**
* Function: getPreviewPoints
*
* Updates the given preview state taking into account the state of the constraint handler.
*/
mxEdgeSegmentHandler.prototype.getPreviewPoints = function(point)
{
if (this.isSource || this.isTarget)
{
return mxElbowEdgeHandler.prototype.getPreviewPoints.apply(this, arguments);
}
else
{
var pts = this.getCurrentPoints();
var last = this.convertPoint(pts[0].clone(), false);
point = this.convertPoint(point.clone(), false);
var result = [];
for (var i = 1; i < pts.length; i++)
{
var pt = this.convertPoint(pts[i].clone(), false);
if (i == this.index)
{
if (Math.round(last.x - pt.x) == 0)
{
last.x = point.x;
pt.x = point.x;
}
if (Math.round(last.y - pt.y) == 0)
{
last.y = point.y;
pt.y = point.y;
}
}
if (i < pts.length - 1)
{
result.push(pt);
}
last = pt;
}
// Replaces single point that intersects with source or target
if (result.length == 1)
{
var source = this.state.getVisibleTerminalState(true);
var target = this.state.getVisibleTerminalState(false);
var scale = this.state.view.getScale();
var tr = this.state.view.getTranslate();
var x = result[0].x * scale + tr.x;
var y = result[0].y * scale + tr.y;
if ((source != null && mxUtils.contains(source, x, y)) ||
(target != null && mxUtils.contains(target, x, y)))
{
result = [point, point];
}
}
return result;
}
};
/**
* Function: updatePreviewState
*
* Overridden to perform optimization of the edge style result.
*/
mxEdgeSegmentHandler.prototype.updatePreviewState = function(edge, point, terminalState, me)
{
mxEdgeHandler.prototype.updatePreviewState.apply(this, arguments);
// Checks and corrects preview by running edge style again
if (!this.isSource && !this.isTarget)
{
point = this.convertPoint(point.clone(), false);
var pts = edge.absolutePoints;
var pt0 = pts[0];
var pt1 = pts[1];
var result = [];
for (var i = 2; i < pts.length; i++)
{
var pt2 = pts[i];
// Merges adjacent segments only if more than 2 to allow for straight edges
if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) &&
(Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0))
{
result.push(this.convertPoint(pt1.clone(), false));
}
pt0 = pt1;
pt1 = pt2;
}
var source = this.state.getVisibleTerminalState(true);
var target = this.state.getVisibleTerminalState(false);
var rpts = this.state.absolutePoints;
// A straight line is represented by 3 handles
if (result.length == 0 && (Math.round(pts[0].x - pts[pts.length - 1].x) == 0 ||
Math.round(pts[0].y - pts[pts.length - 1].y) == 0))
{
result = [point, point];
}
// Handles special case of transitions from straight vertical to routed
else if (pts.length == 5 && result.length == 2 && source != null && target != null &&
rpts != null && Math.round(rpts[0].x - rpts[rpts.length - 1].x) == 0)
{
var view = this.graph.getView();
var scale = view.getScale();
var tr = view.getTranslate();
var y0 = view.getRoutingCenterY(source) / scale - tr.y;
// Use fixed connection point y-coordinate if one exists
var sc = this.graph.getConnectionConstraint(edge, source, true);
if (sc != null)
{
var pt = this.graph.getConnectionPoint(source, sc);
if (pt != null)
{
this.convertPoint(pt, false);
y0 = pt.y;
}
}
var ye = view.getRoutingCenterY(target) / scale - tr.y;
// Use fixed connection point y-coordinate if one exists
var tc = this.graph.getConnectionConstraint(edge, target, false);
if (tc)
{
var pt = this.graph.getConnectionPoint(target, tc);
if (pt != null)
{
this.convertPoint(pt, false);
ye = pt.y;
}
}
result = [new mxPoint(point.x, y0), new mxPoint(point.x, ye)];
}
this.points = result;
// LATER: Check if points and result are different
edge.view.updateFixedTerminalPoints(edge, source, target);
edge.view.updatePoints(edge, this.points, source, target);
edge.view.updateFloatingTerminalPoints(edge, source, target);
}
};
/**
* Overriden to merge edge segments.
*/
mxEdgeSegmentHandler.prototype.connect = function(edge, terminal, isSource, isClone, me)
{
var model = this.graph.getModel();
var geo = model.getGeometry(edge);
var result = null;
// Merges adjacent edge segments
if (geo != null && geo.points != null && geo.points.length > 0)
{
var pts = this.abspoints;
var pt0 = pts[0];
var pt1 = pts[1];
result = [];
for (var i = 2; i < pts.length; i++)
{
var pt2 = pts[i];
// Merges adjacent segments only if more than 2 to allow for straight edges
if ((Math.round(pt0.x - pt1.x) != 0 || Math.round(pt1.x - pt2.x) != 0) &&
(Math.round(pt0.y - pt1.y) != 0 || Math.round(pt1.y - pt2.y) != 0))
{
result.push(this.convertPoint(pt1.clone(), false));
}
pt0 = pt1;
pt1 = pt2;
}
}
model.beginUpdate();
try
{
if (result != null)
{
var geo = model.getGeometry(edge);
if (geo != null)
{
geo = geo.clone();
geo.points = result;
model.setGeometry(edge, geo);
}
}
edge = mxEdgeHandler.prototype.connect.apply(this, arguments);
}
finally
{
model.endUpdate();
}
return edge;
};
/**
* Function: getTooltipForNode
*
* Returns no tooltips.
*/
mxEdgeSegmentHandler.prototype.getTooltipForNode = function(node)
{
return null;
};
/**
* Function: createBends
*
* Adds custom bends for the center of each segment.
*/
mxEdgeSegmentHandler.prototype.start = function(x, y, index)
{
mxEdgeHandler.prototype.start.apply(this, arguments);
if (this.bends != null && this.bends[index] != null &&
!this.isSource && !this.isTarget)
{
mxUtils.setOpacity(this.bends[index].node, 100);
}
};
/**
* Function: createBends
*
* Adds custom bends for the center of each segment.
*/
mxEdgeSegmentHandler.prototype.createBends = function()
{
var bends = [];
// Source
var bend = this.createHandleShape(0);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
var pts = this.getCurrentPoints();
// Waypoints (segment handles)
if (this.graph.isCellBendable(this.state.cell))
{
if (this.points == null)
{
this.points = [];
}
for (var i = 0; i < pts.length - 1; i++)
{
bend = this.createVirtualBend();
bends.push(bend);
var horizontal = Math.round(pts[i].x - pts[i + 1].x) == 0;
// Special case where dy is 0 as well
if (Math.round(pts[i].y - pts[i + 1].y) == 0 && i < pts.length - 2)
{
horizontal = Math.round(pts[i].x - pts[i + 2].x) == 0;
}
bend.setCursor((horizontal) ? 'col-resize' : 'row-resize');
this.points.push(new mxPoint(0,0));
}
}
// Target
var bend = this.createHandleShape(pts.length);
this.initBend(bend);
bend.setCursor(mxConstants.CURSOR_TERMINAL_HANDLE);
bends.push(bend);
return bends;
};
/**
* Function: redraw
*
* Overridden to invoke <refresh> before the redraw.
*/
mxEdgeSegmentHandler.prototype.redraw = function()
{
this.refresh();
mxEdgeHandler.prototype.redraw.apply(this, arguments);
};
/**
* Function: redrawInnerBends
*
* Updates the position of the custom bends.
*/
mxEdgeSegmentHandler.prototype.redrawInnerBends = function(p0, pe)
{
if (this.graph.isCellBendable(this.state.cell))
{
var pts = this.getCurrentPoints();
if (pts != null && pts.length > 1)
{
var straight = false;
// Puts handle in the center of straight edges
if (pts.length == 4 && Math.round(pts[1].x - pts[2].x) == 0 && Math.round(pts[1].y - pts[2].y) == 0)
{
straight = true;
if (Math.round(pts[0].y - pts[pts.length - 1].y) == 0)
{
var cx = pts[0].x + (pts[pts.length - 1].x - pts[0].x) / 2;
pts[1] = new mxPoint(cx, pts[1].y);
pts[2] = new mxPoint(cx, pts[2].y);
}
else
{
var cy = pts[0].y + (pts[pts.length - 1].y - pts[0].y) / 2;
pts[1] = new mxPoint(pts[1].x, cy);
pts[2] = new mxPoint(pts[2].x, cy);
}
}
for (var i = 0; i < pts.length - 1; i++)
{
if (this.bends[i + 1] != null)
{
var p0 = pts[i];
var pe = pts[i + 1];
var pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2);
var b = this.bends[i + 1].bounds;
this.bends[i + 1].bounds = new mxRectangle(Math.floor(pt.x - b.width / 2),
Math.floor(pt.y - b.height / 2), b.width, b.height);
this.bends[i + 1].redraw();
if (this.manageLabelHandle)
{
this.checkLabelHandle(this.bends[i + 1].bounds);
}
}
}
if (straight)
{
mxUtils.setOpacity(this.bends[1].node, this.virtualBendOpacity);
mxUtils.setOpacity(this.bends[3].node, this.virtualBendOpacity);
}
}
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxKeyHandler
*
* Event handler that listens to keystroke events. This is not a singleton,
* however, it is normally only required once if the target is the document
* element (default).
*
* This handler installs a key event listener in the topmost DOM node and
* processes all events that originate from descandants of <mxGraph.container>
* or from the topmost DOM node. The latter means that all unhandled keystrokes
* are handled by this object regardless of the focused state of the <graph>.
*
* Example:
*
* The following example creates a key handler that listens to the delete key
* (46) and deletes the selection cells if the graph is enabled.
*
* (code)
* var keyHandler = new mxKeyHandler(graph);
* keyHandler.bindKey(46, function(evt)
* {
* if (graph.isEnabled())
* {
* graph.removeCells();
* }
* });
* (end)
*
* Keycodes:
*
* See http://tinyurl.com/yp8jgl or http://tinyurl.com/229yqw for a list of
* keycodes or install a key event listener into the document element and print
* the key codes of the respective events to the console.
*
* To support the Command key and the Control key on the Mac, the following
* code can be used.
*
* (code)
* keyHandler.getFunction = function(evt)
* {
* if (evt != null)
* {
* return (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey)) ? this.controlKeys[evt.keyCode] : this.normalKeys[evt.keyCode];
* }
*
* return null;
* };
* (end)
*
* Constructor: mxKeyHandler
*
* Constructs an event handler that executes functions bound to specific
* keystrokes.
*
* Parameters:
*
* graph - Reference to the associated <mxGraph>.
* target - Optional reference to the event target. If null, the document
* element is used as the event target, that is, the object where the key
* event listener is installed.
*/
function mxKeyHandler(graph, target)
{
if (graph != null)
{
this.graph = graph;
this.target = target || document.documentElement;
// Creates the arrays to map from keycodes to functions
this.normalKeys = [];
this.shiftKeys = [];
this.controlKeys = [];
this.controlShiftKeys = [];
this.keydownHandler = mxUtils.bind(this, function(evt)
{
this.keyDown(evt);
});
// Installs the keystroke listener in the target
mxEvent.addListener(this.target, 'keydown', this.keydownHandler);
// Automatically deallocates memory in IE
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload',
mxUtils.bind(this, function()
{
this.destroy();
})
);
}
}
};
/**
* Variable: graph
*
* Reference to the <mxGraph> associated with this handler.
*/
mxKeyHandler.prototype.graph = null;
/**
* Variable: target
*
* Reference to the target DOM, that is, the DOM node where the key event
* listeners are installed.
*/
mxKeyHandler.prototype.target = null;
/**
* Variable: normalKeys
*
* Maps from keycodes to functions for non-pressed control keys.
*/
mxKeyHandler.prototype.normalKeys = null;
/**
* Variable: shiftKeys
*
* Maps from keycodes to functions for pressed shift keys.
*/
mxKeyHandler.prototype.shiftKeys = null;
/**
* Variable: controlKeys
*
* Maps from keycodes to functions for pressed control keys.
*/
mxKeyHandler.prototype.controlKeys = null;
/**
* Variable: controlShiftKeys
*
* Maps from keycodes to functions for pressed control and shift keys.
*/
mxKeyHandler.prototype.controlShiftKeys = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxKeyHandler.prototype.enabled = true;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation returns
* <enabled>.
*/
mxKeyHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling by updating <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxKeyHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: bindKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control key is not pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindKey = function(code, funct)
{
this.normalKeys[code] = funct;
};
/**
* Function: bindShiftKey
*
* Binds the specified keycode to the given function. This binding is used
* if the shift key is pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindShiftKey = function(code, funct)
{
this.shiftKeys[code] = funct;
};
/**
* Function: bindControlKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control key is pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindControlKey = function(code, funct)
{
this.controlKeys[code] = funct;
};
/**
* Function: bindControlShiftKey
*
* Binds the specified keycode to the given function. This binding is used
* if the control and shift key are pressed.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* funct - JavaScript function that takes the key event as an argument.
*/
mxKeyHandler.prototype.bindControlShiftKey = function(code, funct)
{
this.controlShiftKeys[code] = funct;
};
/**
* Function: isControlDown
*
* Returns true if the control key is pressed. This uses <mxEvent.isControlDown>.
*
* Parameters:
*
* evt - Key event whose control key pressed state should be returned.
*/
mxKeyHandler.prototype.isControlDown = function(evt)
{
return mxEvent.isControlDown(evt);
};
/**
* Function: getFunction
*
* Returns the function associated with the given key event or null if no
* function is associated with the given event.
*
* Parameters:
*
* evt - Key event whose associated function should be returned.
*/
mxKeyHandler.prototype.getFunction = function(evt)
{
if (evt != null && !mxEvent.isAltDown(evt))
{
if (this.isControlDown(evt))
{
if (mxEvent.isShiftDown(evt))
{
return this.controlShiftKeys[evt.keyCode];
}
else
{
return this.controlKeys[evt.keyCode];
}
}
else
{
if (mxEvent.isShiftDown(evt))
{
return this.shiftKeys[evt.keyCode];
}
else
{
return this.normalKeys[evt.keyCode];
}
}
}
return null;
};
/**
* Function: isGraphEvent
*
* Returns true if the event should be processed by this handler, that is,
* if the event source is either the target, one of its direct children, a
* descendant of the <mxGraph.container>, or the <mxGraph.cellEditor> of the
* <graph>.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isGraphEvent = function(evt)
{
var source = mxEvent.getSource(evt);
// Accepts events from the target object or
// in-place editing inside graph
if ((source == this.target || source.parentNode == this.target) ||
(this.graph.cellEditor != null && this.graph.cellEditor.isEventSource(evt)))
{
return true;
}
// Accepts events from inside the container
return mxUtils.isAncestorNode(this.graph.container, source);
};
/**
* Function: keyDown
*
* Handles the event by invoking the function bound to the respective keystroke
* if <isEnabledForEvent> returns true for the given event and if
* <isEventIgnored> returns false, except for escape for which
* <isEventIgnored> is not invoked.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.keyDown = function(evt)
{
if (this.isEnabledForEvent(evt))
{
// Cancels the editing if escape is pressed
if (evt.keyCode == 27 /* Escape */)
{
this.escape(evt);
}
// Invokes the function for the keystroke
else if (!this.isEventIgnored(evt))
{
var boundFunction = this.getFunction(evt);
if (boundFunction != null)
{
boundFunction(evt);
mxEvent.consume(evt);
}
}
}
};
/**
* Function: isEnabledForEvent
*
* Returns true if the given event should be handled. <isEventIgnored> is
* called later if the event is not an escape key stroke, in which case
* <escape> is called. This implementation returns true if <isEnabled>
* returns true for both, this handler and <graph>, if the event is not
* consumed and if <isGraphEvent> returns true.
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isEnabledForEvent = function(evt)
{
return (this.graph.isEnabled() && !mxEvent.isConsumed(evt) &&
this.isGraphEvent(evt) && this.isEnabled());
};
/**
* Function: isEventIgnored
*
* Returns true if the given keystroke should be ignored. This returns
* graph.isEditing().
*
* Parameters:
*
* evt - Key event that represents the keystroke.
*/
mxKeyHandler.prototype.isEventIgnored = function(evt)
{
return this.graph.isEditing();
};
/**
* Function: escape
*
* Hook to process ESCAPE keystrokes. This implementation invokes
* <mxGraph.stopEditing> to cancel the current editing, connecting
* and/or other ongoing modifications.
*
* Parameters:
*
* evt - Key event that represents the keystroke. Possible keycode in this
* case is 27 (ESCAPE).
*/
mxKeyHandler.prototype.escape = function(evt)
{
if (this.graph.isEscapeEnabled())
{
this.graph.escape(evt);
}
};
/**
* Function: destroy
*
* Destroys the handler and all its references into the DOM. This does
* normally not need to be called, it is called automatically when the
* window unloads (in IE).
*/
mxKeyHandler.prototype.destroy = function()
{
if (this.target != null && this.keydownHandler != null)
{
mxEvent.removeListener(this.target, 'keydown', this.keydownHandler);
this.keydownHandler = null;
}
this.target = null;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxTooltipHandler
*
* Graph event handler that displays tooltips. <mxGraph.getTooltip> is used to
* get the tooltip for a cell or handle. This handler is built-into
* <mxGraph.tooltipHandler> and enabled using <mxGraph.setTooltips>.
*
* Example:
*
* (code>
* new mxTooltipHandler(graph);
* (end)
*
* Constructor: mxTooltipHandler
*
* Constructs an event handler that displays tooltips with the specified
* delay (in milliseconds). If no delay is specified then a default delay
* of 500 ms (0.5 sec) is used.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* delay - Optional delay in milliseconds.
*/
function mxTooltipHandler(graph, delay)
{
if (graph != null)
{
this.graph = graph;
this.delay = delay || 500;
this.graph.addMouseListener(this);
}
};
/**
* Variable: zIndex
*
* Specifies the zIndex for the tooltip and its shadow. Default is 10005.
*/
mxTooltipHandler.prototype.zIndex = 10005;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxTooltipHandler.prototype.graph = null;
/**
* Variable: delay
*
* Delay to show the tooltip in milliseconds. Default is 500.
*/
mxTooltipHandler.prototype.delay = null;
/**
* Variable: ignoreTouchEvents
*
* Specifies if touch and pen events should be ignored. Default is true.
*/
mxTooltipHandler.prototype.ignoreTouchEvents = true;
/**
* Variable: hideOnHover
*
* Specifies if the tooltip should be hidden if the mouse is moved over the
* current cell. Default is false.
*/
mxTooltipHandler.prototype.hideOnHover = false;
/**
* Variable: destroyed
*
* True if this handler was destroyed using <destroy>.
*/
mxTooltipHandler.prototype.destroyed = false;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxTooltipHandler.prototype.enabled = true;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxTooltipHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*/
mxTooltipHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isHideOnHover
*
* Returns <hideOnHover>.
*/
mxTooltipHandler.prototype.isHideOnHover = function()
{
return this.hideOnHover;
};
/**
* Function: setHideOnHover
*
* Sets <hideOnHover>.
*/
mxTooltipHandler.prototype.setHideOnHover = function(value)
{
this.hideOnHover = value;
};
/**
* Function: init
*
* Initializes the DOM nodes required for this tooltip handler.
*/
mxTooltipHandler.prototype.init = function()
{
if (document.body != null)
{
this.div = document.createElement('div');
this.div.className = 'mxTooltip';
this.div.style.visibility = 'hidden';
document.body.appendChild(this.div);
mxEvent.addGestureListeners(this.div, mxUtils.bind(this, function(evt)
{
this.hideTooltip();
}));
}
};
/**
* Function: getStateForEvent
*
* Returns the <mxCellState> to be used for showing a tooltip for this event.
*/
mxTooltipHandler.prototype.getStateForEvent = function(me)
{
return me.getState();
};
/**
* Function: mouseDown
*
* Handles the event by initiating a rubberband selection. By consuming the
* event all subsequent events of the gesture are redirected to this
* handler.
*/
mxTooltipHandler.prototype.mouseDown = function(sender, me)
{
this.reset(me, false);
this.hideTooltip();
};
/**
* Function: mouseMove
*
* Handles the event by updating the rubberband selection.
*/
mxTooltipHandler.prototype.mouseMove = function(sender, me)
{
if (me.getX() != this.lastX || me.getY() != this.lastY)
{
this.reset(me, true);
var state = this.getStateForEvent(me);
if (this.isHideOnHover() || state != this.state || (me.getSource() != this.node &&
(!this.stateSource || (state != null && this.stateSource ==
(me.isSource(state.shape) || !me.isSource(state.text))))))
{
this.hideTooltip();
}
}
this.lastX = me.getX();
this.lastY = me.getY();
};
/**
* Function: mouseUp
*
* Handles the event by resetting the tooltip timer or hiding the existing
* tooltip.
*/
mxTooltipHandler.prototype.mouseUp = function(sender, me)
{
this.reset(me, true);
this.hideTooltip();
};
/**
* Function: resetTimer
*
* Resets the timer.
*/
mxTooltipHandler.prototype.resetTimer = function()
{
if (this.thread != null)
{
window.clearTimeout(this.thread);
this.thread = null;
}
};
/**
* Function: reset
*
* Resets and/or restarts the timer to trigger the display of the tooltip.
*/
mxTooltipHandler.prototype.reset = function(me, restart, state)
{
if (!this.ignoreTouchEvents || mxEvent.isMouseEvent(me.getEvent()))
{
this.resetTimer();
state = (state != null) ? state : this.getStateForEvent(me);
if (restart && this.isEnabled() && state != null && (this.div == null ||
this.div.style.visibility == 'hidden'))
{
var node = me.getSource();
var x = me.getX();
var y = me.getY();
var stateSource = me.isSource(state.shape) || me.isSource(state.text);
this.thread = window.setTimeout(mxUtils.bind(this, function()
{
if (!this.graph.isEditing() && !this.graph.popupMenuHandler.isMenuShowing() && !this.graph.isMouseDown)
{
// Uses information from inside event cause using the event at
// this (delayed) point in time is not possible in IE as it no
// longer contains the required information (member not found)
var tip = this.graph.getTooltip(state, node, x, y);
this.show(tip, x, y);
this.state = state;
this.node = node;
this.stateSource = stateSource;
}
}), this.delay);
}
}
};
/**
* Function: hide
*
* Hides the tooltip and resets the timer.
*/
mxTooltipHandler.prototype.hide = function()
{
this.resetTimer();
this.hideTooltip();
};
/**
* Function: hideTooltip
*
* Hides the tooltip.
*/
mxTooltipHandler.prototype.hideTooltip = function()
{
if (this.div != null)
{
this.div.style.visibility = 'hidden';
this.div.innerHTML = '';
}
};
/**
* Function: show
*
* Shows the tooltip for the specified cell and optional index at the
* specified location (with a vertical offset of 10 pixels).
*/
mxTooltipHandler.prototype.show = function(tip, x, y)
{
if (!this.destroyed && tip != null && tip.length > 0)
{
// Initializes the DOM nodes if required
if (this.div == null)
{
this.init();
}
var origin = mxUtils.getScrollOrigin();
this.div.style.zIndex = this.zIndex;
this.div.style.left = (x + origin.x) + 'px';
this.div.style.top = (y + mxConstants.TOOLTIP_VERTICAL_OFFSET +
origin.y) + 'px';
if (!mxUtils.isNode(tip))
{
this.div.innerHTML = tip.replace(/\n/g, '<br>');
}
else
{
this.div.innerHTML = '';
this.div.appendChild(tip);
}
this.div.style.visibility = '';
mxUtils.fit(this.div);
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxTooltipHandler.prototype.destroy = function()
{
if (!this.destroyed)
{
this.graph.removeMouseListener(this);
mxEvent.release(this.div);
if (this.div != null && this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.destroyed = true;
this.div = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellTracker
*
* Event handler that highlights cells. Inherits from <mxCellMarker>.
*
* Example:
*
* (code)
* new mxCellTracker(graph, '#00FF00');
* (end)
*
* For detecting dragEnter, dragOver and dragLeave on cells, the following
* code can be used:
*
* (code)
* graph.addMouseListener(
* {
* cell: null,
* mouseDown: function(sender, me) { },
* mouseMove: function(sender, me)
* {
* var tmp = me.getCell();
*
* if (tmp != this.cell)
* {
* if (this.cell != null)
* {
* this.dragLeave(me.getEvent(), this.cell);
* }
*
* this.cell = tmp;
*
* if (this.cell != null)
* {
* this.dragEnter(me.getEvent(), this.cell);
* }
* }
*
* if (this.cell != null)
* {
* this.dragOver(me.getEvent(), this.cell);
* }
* },
* mouseUp: function(sender, me) { },
* dragEnter: function(evt, cell)
* {
* mxLog.debug('dragEnter', cell.value);
* },
* dragOver: function(evt, cell)
* {
* mxLog.debug('dragOver', cell.value);
* },
* dragLeave: function(evt, cell)
* {
* mxLog.debug('dragLeave', cell.value);
* }
* });
* (end)
*
* Constructor: mxCellTracker
*
* Constructs an event handler that highlights cells.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* color - Color of the highlight. Default is blue.
* funct - Optional JavaScript function that is used to override
* <mxCellMarker.getCell>.
*/
function mxCellTracker(graph, color, funct)
{
mxCellMarker.call(this, graph, color);
this.graph.addMouseListener(this);
if (funct != null)
{
this.getCell = funct;
}
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload', mxUtils.bind(this, function()
{
this.destroy();
}));
}
};
/**
* Extends mxCellMarker.
*/
mxUtils.extend(mxCellTracker, mxCellMarker);
/**
* Function: mouseDown
*
* Ignores the event. The event is not consumed.
*/
mxCellTracker.prototype.mouseDown = function(sender, me) { };
/**
* Function: mouseMove
*
* Handles the event by highlighting the cell under the mousepointer if it
* is over the hotspot region of the cell.
*/
mxCellTracker.prototype.mouseMove = function(sender, me)
{
if (this.isEnabled())
{
this.process(me);
}
};
/**
* Function: mouseUp
*
* Handles the event by reseting the highlight.
*/
mxCellTracker.prototype.mouseUp = function(sender, me) { };
/**
* Function: destroy
*
* Destroys the object and all its resources and DOM nodes. This doesn't
* normally need to be called. It is called automatically when the window
* unloads.
*/
mxCellTracker.prototype.destroy = function()
{
if (!this.destroyed)
{
this.destroyed = true;
this.graph.removeMouseListener(this);
mxCellMarker.prototype.destroy.apply(this);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCellHighlight
*
* A helper class to highlight cells. Here is an example for a given cell.
*
* (code)
* var highlight = new mxCellHighlight(graph, '#ff0000', 2);
* highlight.highlight(graph.view.getState(cell)));
* (end)
*
* Constructor: mxCellHighlight
*
* Constructs a cell highlight.
*/
function mxCellHighlight(graph, highlightColor, strokeWidth, dashed)
{
if (graph != null)
{
this.graph = graph;
this.highlightColor = (highlightColor != null) ? highlightColor : mxConstants.DEFAULT_VALID_COLOR;
this.strokeWidth = (strokeWidth != null) ? strokeWidth : mxConstants.HIGHLIGHT_STROKEWIDTH;
this.dashed = (dashed != null) ? dashed : false;
this.opacity = mxConstants.HIGHLIGHT_OPACITY;
// Updates the marker if the graph changes
this.repaintHandler = mxUtils.bind(this, function()
{
// Updates reference to state
if (this.state != null)
{
var tmp = this.graph.view.getState(this.state.cell);
if (tmp == null)
{
this.hide();
}
else
{
this.state = tmp;
this.repaint();
}
}
});
this.graph.getView().addListener(mxEvent.SCALE, this.repaintHandler);
this.graph.getView().addListener(mxEvent.TRANSLATE, this.repaintHandler);
this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.repaintHandler);
// Hides the marker if the current root changes
this.resetHandler = mxUtils.bind(this, function()
{
this.hide();
});
this.graph.getView().addListener(mxEvent.DOWN, this.resetHandler);
this.graph.getView().addListener(mxEvent.UP, this.resetHandler);
}
};
/**
* Variable: keepOnTop
*
* Specifies if the highlights should appear on top of everything
* else in the overlay pane. Default is false.
*/
mxCellHighlight.prototype.keepOnTop = false;
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxCellHighlight.prototype.graph = true;
/**
* Variable: state
*
* Reference to the <mxCellState>.
*/
mxCellHighlight.prototype.state = null;
/**
* Variable: spacing
*
* Specifies the spacing between the highlight for vertices and the vertex.
* Default is 2.
*/
mxCellHighlight.prototype.spacing = 2;
/**
* Variable: resetHandler
*
* Holds the handler that automatically invokes reset if the highlight
* should be hidden.
*/
mxCellHighlight.prototype.resetHandler = null;
/**
* Function: setHighlightColor
*
* Sets the color of the rectangle used to highlight drop targets.
*
* Parameters:
*
* color - String that represents the new highlight color.
*/
mxCellHighlight.prototype.setHighlightColor = function(color)
{
this.highlightColor = color;
if (this.shape != null)
{
this.shape.stroke = color;
}
};
/**
* Function: drawHighlight
*
* Creates and returns the highlight shape for the given state.
*/
mxCellHighlight.prototype.drawHighlight = function()
{
this.shape = this.createShape();
this.repaint();
if (!this.keepOnTop && this.shape.node.parentNode.firstChild != this.shape.node)
{
this.shape.node.parentNode.insertBefore(this.shape.node, this.shape.node.parentNode.firstChild);
}
};
/**
* Function: createShape
*
* Creates and returns the highlight shape for the given state.
*/
mxCellHighlight.prototype.createShape = function()
{
var shape = this.graph.cellRenderer.createShape(this.state);
shape.svgStrokeTolerance = this.graph.tolerance;
shape.points = this.state.absolutePoints;
shape.apply(this.state);
shape.stroke = this.highlightColor;
shape.opacity = this.opacity;
shape.isDashed = this.dashed;
shape.isShadow = false;
shape.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG;
shape.init(this.graph.getView().getOverlayPane());
mxEvent.redirectMouseEvents(shape.node, this.graph, this.state);
if (this.graph.dialect != mxConstants.DIALECT_SVG)
{
shape.pointerEvents = false;
}
else
{
shape.svgPointerEvents = 'stroke';
}
return shape;
};
/**
* Function: repaint
*
* Updates the highlight after a change of the model or view.
*/
mxCellHighlight.prototype.getStrokeWidth = function(state)
{
return this.strokeWidth;
};
/**
* Function: repaint
*
* Updates the highlight after a change of the model or view.
*/
mxCellHighlight.prototype.repaint = function()
{
if (this.state != null && this.shape != null)
{
this.shape.scale = this.state.view.scale;
if (this.graph.model.isEdge(this.state.cell))
{
this.shape.strokewidth = this.getStrokeWidth();
this.shape.points = this.state.absolutePoints;
this.shape.outline = false;
}
else
{
this.shape.bounds = new mxRectangle(this.state.x - this.spacing, this.state.y - this.spacing,
this.state.width + 2 * this.spacing, this.state.height + 2 * this.spacing);
this.shape.rotation = Number(this.state.style[mxConstants.STYLE_ROTATION] || '0');
this.shape.strokewidth = this.getStrokeWidth() / this.state.view.scale;
this.shape.outline = true;
}
// Uses cursor from shape in highlight
if (this.state.shape != null)
{
this.shape.setCursor(this.state.shape.getCursor());
}
// Workaround for event transparency in VML with transparent color
// is to use a non-transparent color with near zero opacity
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
if (this.shape.stroke == 'transparent')
{
// KNOWN: Quirks mode does not seem to catch events if
// we do not force an update of the DOM via a change such
// as mxLog.debug. Since IE6 is EOL we do not add a fix.
this.shape.stroke = 'white';
this.shape.opacity = 1;
}
else
{
this.shape.opacity = this.opacity;
}
}
this.shape.redraw();
}
};
/**
* Function: hide
*
* Resets the state of the cell marker.
*/
mxCellHighlight.prototype.hide = function()
{
this.highlight(null);
};
/**
* Function: mark
*
* Marks the <markedState> and fires a <mark> event.
*/
mxCellHighlight.prototype.highlight = function(state)
{
if (this.state != state)
{
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
this.state = state;
if (this.state != null)
{
this.drawHighlight();
}
}
};
/**
* Function: isHighlightAt
*
* Returns true if this highlight is at the given position.
*/
mxCellHighlight.prototype.isHighlightAt = function(x, y)
{
var hit = false;
// Quirks mode is currently not supported as it used a different coordinate system
if (this.shape != null && document.elementFromPoint != null && !mxClient.IS_QUIRKS)
{
var elt = document.elementFromPoint(x, y);
while (elt != null)
{
if (elt == this.shape.node)
{
hit = true;
break;
}
elt = elt.parentNode;
}
}
return hit;
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxCellHighlight.prototype.destroy = function()
{
this.graph.getView().removeListener(this.resetHandler);
this.graph.getView().removeListener(this.repaintHandler);
this.graph.getModel().removeListener(this.repaintHandler);
if (this.shape != null)
{
this.shape.destroy();
this.shape = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultKeyHandler
*
* Binds keycodes to actionnames in an editor. This aggregates an internal
* <handler> and extends the implementation of <mxKeyHandler.escape> to not
* only cancel the editing, but also hide the properties dialog and fire an
* <mxEditor.escape> event via <editor>. An instance of this class is created
* by <mxEditor> and stored in <mxEditor.keyHandler>.
*
* Example:
*
* Bind the delete key to the delete action in an existing editor.
*
* (code)
* var keyHandler = new mxDefaultKeyHandler(editor);
* keyHandler.bindAction(46, 'delete');
* (end)
*
* Codec:
*
* This class uses the <mxDefaultKeyHandlerCodec> to read configuration
* data into an existing instance. See <mxDefaultKeyHandlerCodec> for a
* description of the configuration format.
*
* Keycodes:
*
* See <mxKeyHandler>.
*
* An <mxEvent.ESCAPE> event is fired via the editor if the escape key is
* pressed.
*
* Constructor: mxDefaultKeyHandler
*
* Constructs a new default key handler for the <mxEditor.graph> in the
* given <mxEditor>. (The editor may be null if a prototypical instance for
* a <mxDefaultKeyHandlerCodec> is created.)
*
* Parameters:
*
* editor - Reference to the enclosing <mxEditor>.
*/
function mxDefaultKeyHandler(editor)
{
if (editor != null)
{
this.editor = editor;
this.handler = new mxKeyHandler(editor.graph);
// Extends the escape function of the internal key
// handle to hide the properties dialog and fire
// the escape event via the editor instance
var old = this.handler.escape;
this.handler.escape = function(evt)
{
old.apply(this, arguments);
editor.hideProperties();
editor.fireEvent(new mxEventObject(mxEvent.ESCAPE, 'event', evt));
};
}
};
/**
* Variable: editor
*
* Reference to the enclosing <mxEditor>.
*/
mxDefaultKeyHandler.prototype.editor = null;
/**
* Variable: handler
*
* Holds the <mxKeyHandler> for key event handling.
*/
mxDefaultKeyHandler.prototype.handler = null;
/**
* Function: bindAction
*
* Binds the specified keycode to the given action in <editor>. The
* optional control flag specifies if the control key must be pressed
* to trigger the action.
*
* Parameters:
*
* code - Integer that specifies the keycode.
* action - Name of the action to execute in <editor>.
* control - Optional boolean that specifies if control must be pressed.
* Default is false.
*/
mxDefaultKeyHandler.prototype.bindAction = function (code, action, control)
{
var keyHandler = mxUtils.bind(this, function()
{
this.editor.execute(action);
});
// Binds the function to control-down keycode
if (control)
{
this.handler.bindControlKey(code, keyHandler);
}
// Binds the function to the normal keycode
else
{
this.handler.bindKey(code, keyHandler);
}
};
/**
* Function: destroy
*
* Destroys the <handler> associated with this object. This does normally
* not need to be called, the <handler> is destroyed automatically when the
* window unloads (in IE) by <mxEditor>.
*/
mxDefaultKeyHandler.prototype.destroy = function ()
{
this.handler.destroy();
this.handler = null;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultPopupMenu
*
* Creates popupmenus for mouse events. This object holds an XML node
* which is a description of the popup menu to be created. In
* <createMenu>, the configuration is applied to the context and
* the resulting menu items are added to the menu dynamically. See
* <createMenu> for a description of the configuration format.
*
* This class does not create the DOM nodes required for the popup menu, it
* only parses an XML description to invoke the respective methods on an
* <mxPopupMenu> each time the menu is displayed.
*
* Codec:
*
* This class uses the <mxDefaultPopupMenuCodec> to read configuration
* data into an existing instance, however, the actual parsing is done
* by this class during program execution, so the format is described
* below.
*
* Constructor: mxDefaultPopupMenu
*
* Constructs a new popupmenu-factory based on given configuration.
*
* Paramaters:
*
* config - XML node that contains the configuration data.
*/
function mxDefaultPopupMenu(config)
{
this.config = config;
};
/**
* Variable: imageBasePath
*
* Base path for all icon attributes in the config. Default is null.
*/
mxDefaultPopupMenu.prototype.imageBasePath = null;
/**
* Variable: config
*
* XML node used as the description of new menu items. This node is
* used in <createMenu> to dynamically create the menu items if their
* respective conditions evaluate to true for the given arguments.
*/
mxDefaultPopupMenu.prototype.config = null;
/**
* Function: createMenu
*
* This function is called from <mxEditor> to add items to the
* given menu based on <config>. The config is a sequence of
* the following nodes and attributes.
*
* Child Nodes:
*
* add - Adds a new menu item. See below for attributes.
* separator - Adds a separator. No attributes.
* condition - Adds a custom condition. Name attribute.
*
* The add-node may have a child node that defines a function to be invoked
* before the action is executed (or instead of an action to be executed).
*
* Attributes:
*
* as - Resource key for the label (needs entry in property file).
* action - Name of the action to execute in enclosing editor.
* icon - Optional icon (relative/absolute URL).
* iconCls - Optional CSS class for the icon.
* if - Optional name of condition that must be true (see below).
* enabled-if - Optional name of condition that specifies if the menu item
* should be enabled.
* name - Name of custom condition. Only for condition nodes.
*
* Conditions:
*
* nocell - No cell under the mouse.
* ncells - More than one cell selected.
* notRoot - Drilling position is other than home.
* cell - Cell under the mouse.
* notEmpty - Exactly one cell with children under mouse.
* expandable - Exactly one expandable cell under mouse.
* collapsable - Exactly one collapsable cell under mouse.
* validRoot - Exactly one cell which is a possible root under mouse.
* swimlane - Exactly one cell which is a swimlane under mouse.
*
* Example:
*
* To add a new item for a given action to the popupmenu:
*
* (code)
* <mxDefaultPopupMenu as="popupHandler">
* <add as="delete" action="delete" icon="images/delete.gif" if="cell"/>
* </mxDefaultPopupMenu>
* (end)
*
* To add a new item for a custom function:
*
* (code)
* <mxDefaultPopupMenu as="popupHandler">
* <add as="action1"><![CDATA[
* function (editor, cell, evt)
* {
* editor.execute('action1', cell, 'myArg');
* }
* ]]></add>
* </mxDefaultPopupMenu>
* (end)
*
* The above example invokes action1 with an additional third argument via
* the editor instance. The third argument is passed to the function that
* defines action1. If the add-node has no action-attribute, then only the
* function defined in the text content is executed, otherwise first the
* function and then the action defined in the action-attribute is
* executed. The function in the text content has 3 arguments, namely the
* <mxEditor> instance, the <mxCell> instance under the mouse, and the
* native mouse event.
*
* Custom Conditions:
*
* To add a new condition for popupmenu items:
*
* (code)
* <condition name="condition1"><![CDATA[
* function (editor, cell, evt)
* {
* return cell != null;
* }
* ]]></condition>
* (end)
*
* The new condition can then be used in any item as follows:
*
* (code)
* <add as="action1" action="action1" icon="action1.gif" if="condition1"/>
* (end)
*
* The order in which the items and conditions appear is not significant as
* all connditions are evaluated before any items are created.
*
* Parameters:
*
* editor - Enclosing <mxEditor> instance.
* menu - <mxPopupMenu> that is used for adding items and separators.
* cell - Optional <mxCell> which is under the mousepointer.
* evt - Optional mouse event which triggered the menu.
*/
mxDefaultPopupMenu.prototype.createMenu = function(editor, menu, cell, evt)
{
if (this.config != null)
{
var conditions = this.createConditions(editor, cell, evt);
var item = this.config.firstChild;
this.addItems(editor, menu, cell, evt, conditions, item, null);
}
};
/**
* Function: addItems
*
* Recursively adds the given items and all of its children into the given menu.
*
* Parameters:
*
* editor - Enclosing <mxEditor> instance.
* menu - <mxPopupMenu> that is used for adding items and separators.
* cell - Optional <mxCell> which is under the mousepointer.
* evt - Optional mouse event which triggered the menu.
* conditions - Array of names boolean conditions.
* item - XML node that represents the current menu item.
* parent - DOM node that represents the parent menu item.
*/
mxDefaultPopupMenu.prototype.addItems = function(editor, menu, cell, evt, conditions, item, parent)
{
var addSeparator = false;
while (item != null)
{
if (item.nodeName == 'add')
{
var condition = item.getAttribute('if');
if (condition == null || conditions[condition])
{
var as = item.getAttribute('as');
as = mxResources.get(as) || as;
var funct = mxUtils.eval(mxUtils.getTextContent(item));
var action = item.getAttribute('action');
var icon = item.getAttribute('icon');
var iconCls = item.getAttribute('iconCls');
var enabledCond = item.getAttribute('enabled-if');
var enabled = enabledCond == null || conditions[enabledCond];
if (addSeparator)
{
menu.addSeparator(parent);
addSeparator = false;
}
if (icon != null && this.imageBasePath)
{
icon = this.imageBasePath + icon;
}
var row = this.addAction(menu, editor, as, icon, funct, action, cell, parent, iconCls, enabled);
this.addItems(editor, menu, cell, evt, conditions, item.firstChild, row);
}
}
else if (item.nodeName == 'separator')
{
addSeparator = true;
}
item = item.nextSibling;
}
};
/**
* Function: addAction
*
* Helper method to bind an action to a new menu item.
*
* Parameters:
*
* menu - <mxPopupMenu> that is used for adding items and separators.
* editor - Enclosing <mxEditor> instance.
* lab - String that represents the label of the menu item.
* icon - Optional URL that represents the icon of the menu item.
* action - Optional name of the action to execute in the given editor.
* funct - Optional function to execute before the optional action. The
* function takes an <mxEditor>, the <mxCell> under the mouse and the
* mouse event that triggered the call.
* cell - Optional <mxCell> to use as an argument for the action.
* parent - DOM node that represents the parent menu item.
* iconCls - Optional CSS class for the menu icon.
* enabled - Optional boolean that specifies if the menu item is enabled.
* Default is true.
*/
mxDefaultPopupMenu.prototype.addAction = function(menu, editor, lab, icon, funct, action, cell, parent, iconCls, enabled)
{
var clickHandler = function(evt)
{
if (typeof(funct) == 'function')
{
funct.call(editor, editor, cell, evt);
}
if (action != null)
{
editor.execute(action, cell, evt);
}
};
return menu.addItem(lab, icon, clickHandler, parent, iconCls, enabled);
};
/**
* Function: createConditions
*
* Evaluates the default conditions for the given context.
*/
mxDefaultPopupMenu.prototype.createConditions = function(editor, cell, evt)
{
// Creates array with conditions
var model = editor.graph.getModel();
var childCount = model.getChildCount(cell);
// Adds some frequently used conditions
var conditions = [];
conditions['nocell'] = cell == null;
conditions['ncells'] = editor.graph.getSelectionCount() > 1;
conditions['notRoot'] = model.getRoot() !=
model.getParent(editor.graph.getDefaultParent());
conditions['cell'] = cell != null;
var isCell = cell != null && editor.graph.getSelectionCount() == 1;
conditions['nonEmpty'] = isCell && childCount > 0;
conditions['expandable'] = isCell && editor.graph.isCellFoldable(cell, false);
conditions['collapsable'] = isCell && editor.graph.isCellFoldable(cell, true);
conditions['validRoot'] = isCell && editor.graph.isValidRoot(cell);
conditions['emptyValidRoot'] = conditions['validRoot'] && childCount == 0;
conditions['swimlane'] = isCell && editor.graph.isSwimlane(cell);
// Evaluates dynamic conditions from config file
var condNodes = this.config.getElementsByTagName('condition');
for (var i=0; i<condNodes.length; i++)
{
var funct = mxUtils.eval(mxUtils.getTextContent(condNodes[i]));
var name = condNodes[i].getAttribute('name');
if (name != null && typeof(funct) == 'function')
{
conditions[name] = funct(editor, cell, evt);
}
}
return conditions;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultToolbar
*
* Toolbar for the editor. This modifies the state of the graph
* or inserts new cells upon mouse clicks.
*
* Example:
*
* Create a toolbar with a button to copy the selection into the clipboard,
* and a combo box with one action to paste the selection from the clipboard
* into the graph.
*
* (code)
* var toolbar = new mxDefaultToolbar(container, editor);
* toolbar.addItem('Copy', null, 'copy');
*
* var combo = toolbar.addActionCombo('More actions...');
* toolbar.addActionOption(combo, 'Paste', 'paste');
* (end)
*
* Codec:
*
* This class uses the <mxDefaultToolbarCodec> to read configuration
* data into an existing instance. See <mxDefaultToolbarCodec> for a
* description of the configuration format.
*
* Constructor: mxDefaultToolbar
*
* Constructs a new toolbar for the given container and editor. The
* container and editor may be null if a prototypical instance for a
* <mxDefaultKeyHandlerCodec> is created.
*
* Parameters:
*
* container - DOM node that contains the toolbar.
* editor - Reference to the enclosing <mxEditor>.
*/
function mxDefaultToolbar(container, editor)
{
this.editor = editor;
if (container != null && editor != null)
{
this.init(container);
}
};
/**
* Variable: editor
*
* Reference to the enclosing <mxEditor>.
*/
mxDefaultToolbar.prototype.editor = null;
/**
* Variable: toolbar
*
* Holds the internal <mxToolbar>.
*/
mxDefaultToolbar.prototype.toolbar = null;
/**
* Variable: resetHandler
*
* Reference to the function used to reset the <toolbar>.
*/
mxDefaultToolbar.prototype.resetHandler = null;
/**
* Variable: spacing
*
* Defines the spacing between existing and new vertices in
* gridSize units when a new vertex is dropped on an existing
* cell. Default is 4 (40 pixels).
*/
mxDefaultToolbar.prototype.spacing = 4;
/**
* Variable: connectOnDrop
*
* Specifies if elements should be connected if new cells are dropped onto
* connectable elements. Default is false.
*/
mxDefaultToolbar.prototype.connectOnDrop = false;
/**
* Function: init
*
* Constructs the <toolbar> for the given container and installs a listener
* that updates the <mxEditor.insertFunction> on <editor> if an item is
* selected in the toolbar. This assumes that <editor> is not null.
*
* Parameters:
*
* container - DOM node that contains the toolbar.
*/
mxDefaultToolbar.prototype.init = function(container)
{
if (container != null)
{
this.toolbar = new mxToolbar(container);
// Installs the insert function in the editor if an item is
// selected in the toolbar
this.toolbar.addListener(mxEvent.SELECT, mxUtils.bind(this, function(sender, evt)
{
var funct = evt.getProperty('function');
if (funct != null)
{
this.editor.insertFunction = mxUtils.bind(this, function()
{
funct.apply(this, arguments);
this.toolbar.resetMode();
});
}
else
{
this.editor.insertFunction = null;
}
}));
// Resets the selected tool after a doubleclick or escape keystroke
this.resetHandler = mxUtils.bind(this, function()
{
if (this.toolbar != null)
{
this.toolbar.resetMode(true);
}
});
this.editor.graph.addListener(mxEvent.DOUBLE_CLICK, this.resetHandler);
this.editor.addListener(mxEvent.ESCAPE, this.resetHandler);
}
};
/**
* Function: addItem
*
* Adds a new item that executes the given action in <editor>. The title,
* icon and pressedIcon are used to display the toolbar item.
*
* Parameters:
*
* title - String that represents the title (tooltip) for the item.
* icon - URL of the icon to be used for displaying the item.
* action - Name of the action to execute when the item is clicked.
* pressed - Optional URL of the icon for the pressed state.
*/
mxDefaultToolbar.prototype.addItem = function(title, icon, action, pressed)
{
var clickHandler = mxUtils.bind(this, function()
{
if (action != null && action.length > 0)
{
this.editor.execute(action);
}
});
return this.toolbar.addItem(title, icon, clickHandler, pressed);
};
/**
* Function: addSeparator
*
* Adds a vertical separator using the optional icon.
*
* Parameters:
*
* icon - Optional URL of the icon that represents the vertical separator.
* Default is <mxClient.imageBasePath> + '/separator.gif'.
*/
mxDefaultToolbar.prototype.addSeparator = function(icon)
{
icon = icon || mxClient.imageBasePath + '/separator.gif';
this.toolbar.addSeparator(icon);
};
/**
* Function: addCombo
*
* Helper method to invoke <mxToolbar.addCombo> on <toolbar> and return the
* resulting DOM node.
*/
mxDefaultToolbar.prototype.addCombo = function()
{
return this.toolbar.addCombo();
};
/**
* Function: addActionCombo
*
* Helper method to invoke <mxToolbar.addActionCombo> on <toolbar> using
* the given title and return the resulting DOM node.
*
* Parameters:
*
* title - String that represents the title of the combo.
*/
mxDefaultToolbar.prototype.addActionCombo = function(title)
{
return this.toolbar.addActionCombo(title);
};
/**
* Function: addActionOption
*
* Binds the given action to a option with the specified label in the
* given combo. Combo is an object returned from an earlier call to
* <addCombo> or <addActionCombo>.
*
* Parameters:
*
* combo - DOM node that represents the combo box.
* title - String that represents the title of the combo.
* action - Name of the action to execute in <editor>.
*/
mxDefaultToolbar.prototype.addActionOption = function(combo, title, action)
{
var clickHandler = mxUtils.bind(this, function()
{
this.editor.execute(action);
});
this.addOption(combo, title, clickHandler);
};
/**
* Function: addOption
*
* Helper method to invoke <mxToolbar.addOption> on <toolbar> and return
* the resulting DOM node that represents the option.
*
* Parameters:
*
* combo - DOM node that represents the combo box.
* title - String that represents the title of the combo.
* value - Object that represents the value of the option.
*/
mxDefaultToolbar.prototype.addOption = function(combo, title, value)
{
return this.toolbar.addOption(combo, title, value);
};
/**
* Function: addMode
*
* Creates an item for selecting the given mode in the <editor>'s graph.
* Supported modenames are select, connect and pan.
*
* Parameters:
*
* title - String that represents the title of the item.
* icon - URL of the icon that represents the item.
* mode - String that represents the mode name to be used in
* <mxEditor.setMode>.
* pressed - Optional URL of the icon that represents the pressed state.
* funct - Optional JavaScript function that takes the <mxEditor> as the
* first and only argument that is executed after the mode has been
* selected.
*/
mxDefaultToolbar.prototype.addMode = function(title, icon, mode, pressed, funct)
{
var clickHandler = mxUtils.bind(this, function()
{
this.editor.setMode(mode);
if (funct != null)
{
funct(this.editor);
}
});
return this.toolbar.addSwitchMode(title, icon, clickHandler, pressed);
};
/**
* Function: addPrototype
*
* Creates an item for inserting a clone of the specified prototype cell into
* the <editor>'s graph. The ptype may either be a cell or a function that
* returns a cell.
*
* Parameters:
*
* title - String that represents the title of the item.
* icon - URL of the icon that represents the item.
* ptype - Function or object that represents the prototype cell. If ptype
* is a function then it is invoked with no arguments to create new
* instances.
* pressed - Optional URL of the icon that represents the pressed state.
* insert - Optional JavaScript function that handles an insert of the new
* cell. This function takes the <mxEditor>, new cell to be inserted, mouse
* event and optional <mxCell> under the mouse pointer as arguments.
* toggle - Optional boolean that specifies if the item can be toggled.
* Default is true.
*/
mxDefaultToolbar.prototype.addPrototype = function(title, icon, ptype, pressed, insert, toggle)
{
// Creates a wrapper function that is in charge of constructing
// the new cell instance to be inserted into the graph
var factory = mxUtils.bind(this, function()
{
if (typeof(ptype) == 'function')
{
return ptype();
}
else if (ptype != null)
{
return this.editor.graph.cloneCell(ptype);
}
return null;
});
// Defines the function for a click event on the graph
// after this item has been selected in the toolbar
var clickHandler = mxUtils.bind(this, function(evt, cell)
{
if (typeof(insert) == 'function')
{
insert(this.editor, factory(), evt, cell);
}
else
{
this.drop(factory(), evt, cell);
}
this.toolbar.resetMode();
mxEvent.consume(evt);
});
var img = this.toolbar.addMode(title, icon, clickHandler, pressed, null, toggle);
// Creates a wrapper function that calls the click handler without
// the graph argument
var dropHandler = function(graph, evt, cell)
{
clickHandler(evt, cell);
};
this.installDropHandler(img, dropHandler);
return img;
};
/**
* Function: drop
*
* Handles a drop from a toolbar item to the graph. The given vertex
* represents the new cell to be inserted. This invokes <insert> or
* <connect> depending on the given target cell.
*
* Parameters:
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* target - Optional <mxCell> that represents the drop target.
*/
mxDefaultToolbar.prototype.drop = function(vertex, evt, target)
{
var graph = this.editor.graph;
var model = graph.getModel();
if (target == null ||
model.isEdge(target) ||
!this.connectOnDrop ||
!graph.isCellConnectable(target))
{
while (target != null &&
!graph.isValidDropTarget(target, [vertex], evt))
{
target = model.getParent(target);
}
this.insert(vertex, evt, target);
}
else
{
this.connect(vertex, evt, target);
}
};
/**
* Function: insert
*
* Handles a drop by inserting the given vertex into the given parent cell
* or the default parent if no parent is specified.
*
* Parameters:
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* parent - Optional <mxCell> that represents the parent.
*/
mxDefaultToolbar.prototype.insert = function(vertex, evt, target)
{
var graph = this.editor.graph;
if (graph.canImportCell(vertex))
{
var x = mxEvent.getClientX(evt);
var y = mxEvent.getClientY(evt);
var pt = mxUtils.convertPoint(graph.container, x, y);
// Splits the target edge or inserts into target group
if (graph.isSplitEnabled() &&
graph.isSplitTarget(target, [vertex], evt))
{
return graph.splitEdge(target, [vertex], null, pt.x, pt.y);
}
else
{
return this.editor.addVertex(target, vertex, pt.x, pt.y);
}
}
return null;
};
/**
* Function: connect
*
* Handles a drop by connecting the given vertex to the given source cell.
*
* vertex - <mxCell> to be inserted.
* evt - Mouse event that represents the drop.
* source - Optional <mxCell> that represents the source terminal.
*/
mxDefaultToolbar.prototype.connect = function(vertex, evt, source)
{
var graph = this.editor.graph;
var model = graph.getModel();
if (source != null &&
graph.isCellConnectable(vertex) &&
graph.isEdgeValid(null, source, vertex))
{
var edge = null;
model.beginUpdate();
try
{
var geo = model.getGeometry(source);
var g = model.getGeometry(vertex).clone();
// Moves the vertex away from the drop target that will
// be used as the source for the new connection
g.x = geo.x + (geo.width - g.width) / 2;
g.y = geo.y + (geo.height - g.height) / 2;
var step = this.spacing * graph.gridSize;
var dist = model.getDirectedEdgeCount(source, true) * 20;
if (this.editor.horizontalFlow)
{
g.x += (g.width + geo.width) / 2 + step + dist;
}
else
{
g.y += (g.height + geo.height) / 2 + step + dist;
}
vertex.setGeometry(g);
// Fires two add-events with the code below - should be fixed
// to only fire one add event for both inserts
var parent = model.getParent(source);
graph.addCell(vertex, parent);
graph.constrainChild(vertex);
// Creates the edge using the editor instance and calls
// the second function that fires an add event
edge = this.editor.createEdge(source, vertex);
if (model.getGeometry(edge) == null)
{
var edgeGeometry = new mxGeometry();
edgeGeometry.relative = true;
model.setGeometry(edge, edgeGeometry);
}
graph.addEdge(edge, parent, source, vertex);
}
finally
{
model.endUpdate();
}
graph.setSelectionCells([vertex, edge]);
graph.scrollCellToVisible(vertex);
}
};
/**
* Function: installDropHandler
*
* Makes the given img draggable using the given function for handling a
* drop event.
*
* Parameters:
*
* img - DOM node that represents the image.
* dropHandler - Function that handles a drop of the image.
*/
mxDefaultToolbar.prototype.installDropHandler = function (img, dropHandler)
{
var sprite = document.createElement('img');
sprite.setAttribute('src', img.getAttribute('src'));
// Handles delayed loading of the images
var loader = mxUtils.bind(this, function(evt)
{
// Preview uses the image node with double size. Later this can be
// changed to use a separate preview and guides, but for this the
// dropHandler must use the additional x- and y-arguments and the
// dragsource which makeDraggable returns much be configured to
// use guides via mxDragSource.isGuidesEnabled.
sprite.style.width = (2 * img.offsetWidth) + 'px';
sprite.style.height = (2 * img.offsetHeight) + 'px';
mxUtils.makeDraggable(img, this.editor.graph, dropHandler,
sprite);
mxEvent.removeListener(sprite, 'load', loader);
});
if (mxClient.IS_IE)
{
loader();
}
else
{
mxEvent.addListener(sprite, 'load', loader);
}
};
/**
* Function: destroy
*
* Destroys the <toolbar> associated with this object and removes all
* installed listeners. This does normally not need to be called, the
* <toolbar> is destroyed automatically when the window unloads (in IE) by
* <mxEditor>.
*/
mxDefaultToolbar.prototype.destroy = function ()
{
if (this.resetHandler != null)
{
this.editor.graph.removeListener('dblclick', this.resetHandler);
this.editor.removeListener('escape', this.resetHandler);
this.resetHandler = null;
}
if (this.toolbar != null)
{
this.toolbar.destroy();
this.toolbar = null;
}
};
/**
* Copyright (c) 2006-2019, JGraph Ltd
* Copyright (c) 2006-2019, draw.io AG
*/
/**
* Class: mxEditor
*
* Extends <mxEventSource> to implement a application wrapper for a graph that
* adds <actions>, I/O using <mxCodec>, auto-layout using <mxLayoutManager>,
* command history using <undoManager>, and standard dialogs and widgets, eg.
* properties, help, outline, toolbar, and popupmenu. It also adds <templates>
* to be used as cells in toolbars, auto-validation using the <validation>
* flag, attribute cycling using <cycleAttributeValues>, higher-level events
* such as <root>, and backend integration using <urlPost> and <urlImage>.
*
* Actions:
*
* Actions are functions stored in the <actions> array under their names. The
* functions take the <mxEditor> as the first, and an optional <mxCell> as the
* second argument and are invoked using <execute>. Any additional arguments
* passed to execute are passed on to the action as-is.
*
* A list of built-in actions is available in the <addActions> description.
*
* Read/write Diagrams:
*
* To read a diagram from an XML string, for example from a textfield within the
* page, the following code is used:
*
* (code)
* var doc = mxUtils.parseXML(xmlString);
* var node = doc.documentElement;
* editor.readGraphModel(node);
* (end)
*
* For reading a diagram from a remote location, use the <open> method.
*
* To save diagrams in XML on a server, you can set the <urlPost> variable.
* This variable will be used in <getUrlPost> to construct a URL for the post
* request that is issued in the <save> method. The post request contains the
* XML representation of the diagram as returned by <writeGraphModel> in the
* xml parameter.
*
* On the server side, the post request is processed using standard
* technologies such as Java Servlets, CGI, .NET or ASP.
*
* Here are some examples of processing a post request in various languages.
*
* - Java: URLDecoder.decode(request.getParameter("xml"), "UTF-8").replace("\n", "&#xa;")
*
* Note that the linefeeds should only be replaced if the XML is
* processed in Java, for example when creating an image, but not
* if the XML is passed back to the client-side.
*
* - .NET: HttpUtility.UrlDecode(context.Request.Params["xml"])
* - PHP: urldecode($_POST["xml"])
*
* Creating images:
*
* A backend (Java, PHP or C#) is required for creating images. The
* distribution contains an example for each backend (ImageHandler.java,
* ImageHandler.cs and graph.php). More information about using a backend
* to create images can be found in the readme.html files. Note that the
* preview is implemented using VML/SVG in the browser and does not require
* a backend. The backend is only required to creates images (bitmaps).
*
* Special characters:
*
* Note There are five characters that should always appear in XML content as
* escapes, so that they do not interact with the syntax of the markup. These
* are part of the language for all documents based on XML and for HTML.
*
* - &lt; (<)
* - &gt; (>)
* - &amp; (&)
* - &quot; (")
* - &apos; (')
*
* Although it is part of the XML language, &apos; is not defined in HTML.
* For this reason the XHTML specification recommends instead the use of
* &#39; if text may be passed to a HTML user agent.
*
* If you are having problems with special characters on the server-side then
* you may want to try the <escapePostData> flag.
*
* For converting decimal escape sequences inside strings, a user has provided
* us with the following function:
*
* (code)
* function html2js(text)
* {
* var entitySearch = /&#[0-9]+;/;
* var entity;
*
* while (entity = entitySearch.exec(text))
* {
* var charCode = entity[0].substring(2, entity[0].length -1);
* text = text.substring(0, entity.index)
* + String.fromCharCode(charCode)
* + text.substring(entity.index + entity[0].length);
* }
*
* return text;
* }
* (end)
*
* Otherwise try using hex escape sequences and the built-in unescape function
* for converting such strings.
*
* Local Files:
*
* For saving and opening local files, no standardized method exists that
* works across all browsers. The recommended way of dealing with local files
* is to create a backend that streams the XML data back to the browser (echo)
* as an attachment so that a Save-dialog is displayed on the client-side and
* the file can be saved to the local disk.
*
* For example, in PHP the code that does this looks as follows.
*
* (code)
* $xml = stripslashes($_POST["xml"]);
* header("Content-Disposition: attachment; filename=\"diagram.xml\"");
* echo($xml);
* (end)
*
* To open a local file, the file should be uploaded via a form in the browser
* and then opened from the server in the editor.
*
* Cell Properties:
*
* The properties displayed in the properties dialog are the attributes and
* values of the cell's user object, which is an XML node. The XML node is
* defined in the templates section of the config file.
*
* The templates are stored in <mxEditor.templates> and contain cells which
* are cloned at insertion time to create new vertices by use of drag and
* drop from the toolbar. Each entry in the toolbar for adding a new vertex
* must refer to an existing template.
*
* In the following example, the task node is a business object and only the
* mxCell node and its mxGeometry child contain graph information:
*
* (code)
* <Task label="Task" description="">
* <mxCell vertex="true">
* <mxGeometry as="geometry" width="72" height="32"/>
* </mxCell>
* </Task>
* (end)
*
* The idea is that the XML representation is inverse from the in-memory
* representation: The outer XML node is the user object and the inner node is
* the cell. This means the user object of the cell is the Task node with no
* children for the above example:
*
* (code)
* <Task label="Task" description=""/>
* (end)
*
* The Task node can have any tag name, attributes and child nodes. The
* <mxCodec> will use the XML hierarchy as the user object, while removing the
* "known annotations", such as the mxCell node. At save-time the cell data
* will be "merged" back into the user object. The user object is only modified
* via the properties dialog during the lifecycle of the cell.
*
* In the default implementation of <createProperties>, the user object's
* attributes are put into a form for editing. Attributes are changed using
* the <mxCellAttributeChange> action in the model. The dialog can be replaced
* by overriding the <createProperties> hook or by replacing the showProperties
* action in <actions>. Alternatively, the entry in the config file's popupmenu
* section can be modified to invoke a different action.
*
* If you want to displey the properties dialog on a doubleclick, you can set
* <mxEditor.dblClickAction> to showProperties as follows:
*
* (code)
* editor.dblClickAction = 'showProperties';
* (end)
*
* Popupmenu and Toolbar:
*
* The toolbar and popupmenu are typically configured using the respective
* sections in the config file, that is, the popupmenu is defined as follows:
*
* (code)
* <mxEditor>
* <mxDefaultPopupMenu as="popupHandler">
* <add as="cut" action="cut" icon="images/cut.gif"/>
* ...
* (end)
*
* New entries can be added to the toolbar by inserting an add-node into the
* above configuration. Existing entries may be removed and changed by
* modifying or removing the respective entries in the configuration.
* The configuration is read by the <mxDefaultPopupMenuCodec>, the format of the
* configuration is explained in <mxDefaultPopupMenu.decode>.
*
* The toolbar is defined in the mxDefaultToolbar section. Items can be added
* and removed in this section.
*
* (code)
* <mxEditor>
* <mxDefaultToolbar>
* <add as="save" action="save" icon="images/save.gif"/>
* <add as="Swimlane" template="swimlane" icon="images/swimlane.gif"/>
* ...
* (end)
*
* The format of the configuration is described in
* <mxDefaultToolbarCodec.decode>.
*
* Ids:
*
* For the IDs, there is an implicit behaviour in <mxCodec>: It moves the Id
* from the cell to the user object at encoding time and vice versa at decoding
* time. For example, if the Task node from above has an id attribute, then
* the <mxCell.id> of the corresponding cell will have this value. If there
* is no Id collision in the model, then the cell may be retrieved using this
* Id with the <mxGraphModel.getCell> function. If there is a collision, a new
* Id will be created for the cell using <mxGraphModel.createId>. At encoding
* time, this new Id will replace the value previously stored under the id
* attribute in the Task node.
*
* See <mxEditorCodec>, <mxDefaultToolbarCodec> and <mxDefaultPopupMenuCodec>
* for information about configuring the editor and user interface.
*
* Programmatically inserting cells:
*
* For inserting a new cell, say, by clicking a button in the document,
* the following code can be used. This requires an reference to the editor.
*
* (code)
* var userObject = new Object();
* var parent = editor.graph.getDefaultParent();
* var model = editor.graph.model;
* model.beginUpdate();
* try
* {
* editor.graph.insertVertex(parent, null, userObject, 20, 20, 80, 30);
* }
* finally
* {
* model.endUpdate();
* }
* (end)
*
* If a template cell from the config file should be inserted, then a clone
* of the template can be created as follows. The clone is then inserted using
* the add function instead of addVertex.
*
* (code)
* var template = editor.templates['task'];
* var clone = editor.graph.model.cloneCell(template);
* (end)
*
* Resources:
*
* resources/editor - Language resources for mxEditor
*
* Callback: onInit
*
* Called from within the constructor. In the callback,
* "this" refers to the editor instance.
*
* Cookie: mxgraph=seen
*
* Set when the editor is started. Never expires. Use
* <resetFirstTime> to reset this cookie. This cookie
* only exists if <onInit> is implemented.
*
* Event: mxEvent.OPEN
*
* Fires after a file was opened in <open>. The <code>filename</code> property
* contains the filename that was used. The same value is also available in
* <filename>.
*
* Event: mxEvent.SAVE
*
* Fires after the current file was saved in <save>. The <code>url</code>
* property contains the URL that was used for saving.
*
* Event: mxEvent.POST
*
* Fires if a successful response was received in <postDiagram>. The
* <code>request</code> property contains the <mxXmlRequest>, the
* <code>url</code> and <code>data</code> properties contain the URL and the
* data that were used in the post request.
*
* Event: mxEvent.ROOT
*
* Fires when the current root has changed, or when the title of the current
* root has changed. This event has no properties.
*
* Event: mxEvent.BEFORE_ADD_VERTEX
*
* Fires before a vertex is added in <addVertex>. The <code>vertex</code>
* property contains the new vertex and the <code>parent</code> property
* contains its parent.
*
* Event: mxEvent.ADD_VERTEX
*
* Fires between begin- and endUpdate in <addVertex>. The <code>vertex</code>
* property contains the vertex that is being inserted.
*
* Event: mxEvent.AFTER_ADD_VERTEX
*
* Fires after a vertex was inserted and selected in <addVertex>. The
* <code>vertex</code> property contains the new vertex.
*
* Example:
*
* For starting an in-place edit after a new vertex has been added to the
* graph, the following code can be used.
*
* (code)
* editor.addListener(mxEvent.AFTER_ADD_VERTEX, function(sender, evt)
* {
* var vertex = evt.getProperty('vertex');
*
* if (editor.graph.isCellEditable(vertex))
* {
* editor.graph.startEditingAtCell(vertex);
* }
* });
* (end)
*
* Event: mxEvent.ESCAPE
*
* Fires when the escape key is pressed. The <code>event</code> property
* contains the key event.
*
* Constructor: mxEditor
*
* Constructs a new editor. This function invokes the <onInit> callback
* upon completion.
*
* Example:
*
* (code)
* var config = mxUtils.load('config/diagrameditor.xml').getDocumentElement();
* var editor = new mxEditor(config);
* (end)
*
* Parameters:
*
* config - Optional XML node that contains the configuration.
*/
function mxEditor(config)
{
this.actions = [];
this.addActions();
// Executes the following only if a document has been instanciated.
// That is, don't execute when the editorcodec is setup.
if (document.body != null)
{
// Defines instance fields
this.cycleAttributeValues = [];
this.popupHandler = new mxDefaultPopupMenu();
this.undoManager = new mxUndoManager();
// Creates the graph and toolbar without the containers
this.graph = this.createGraph();
this.toolbar = this.createToolbar();
// Creates the global keyhandler (requires graph instance)
this.keyHandler = new mxDefaultKeyHandler(this);
// Configures the editor using the URI
// which was passed to the ctor
this.configure(config);
// Assigns the swimlaneIndicatorColorAttribute on the graph
this.graph.swimlaneIndicatorColorAttribute = this.cycleAttributeName;
// Checks if the <onInit> hook has been set
if (this.onInit != null)
{
// Invokes the <onInit> hook
this.onInit();
}
// Automatic deallocation of memory
if (mxClient.IS_IE)
{
mxEvent.addListener(window, 'unload', mxUtils.bind(this, function()
{
this.destroy();
}));
}
}
};
/**
* Installs the required language resources at class
* loading time.
*/
if (mxLoadResources)
{
mxResources.add(mxClient.basePath + '/resources/editor');
}
else
{
mxClient.defaultBundles.push(mxClient.basePath + '/resources/editor');
}
/**
* Extends mxEventSource.
*/
mxEditor.prototype = new mxEventSource();
mxEditor.prototype.constructor = mxEditor;
/**
* Group: Controls and Handlers
*/
/**
* Variable: askZoomResource
*
* Specifies the resource key for the zoom dialog. If the resource for this
* key does not exist then the value is used as the error message. Default
* is 'askZoom'.
*/
mxEditor.prototype.askZoomResource = (mxClient.language != 'none') ? 'askZoom' : '';
/**
* Variable: lastSavedResource
*
* Specifies the resource key for the last saved info. If the resource for
* this key does not exist then the value is used as the error message.
* Default is 'lastSaved'.
*/
mxEditor.prototype.lastSavedResource = (mxClient.language != 'none') ? 'lastSaved' : '';
/**
* Variable: currentFileResource
*
* Specifies the resource key for the current file info. If the resource for
* this key does not exist then the value is used as the error message.
* Default is 'currentFile'.
*/
mxEditor.prototype.currentFileResource = (mxClient.language != 'none') ? 'currentFile' : '';
/**
* Variable: propertiesResource
*
* Specifies the resource key for the properties window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'properties'.
*/
mxEditor.prototype.propertiesResource = (mxClient.language != 'none') ? 'properties' : '';
/**
* Variable: tasksResource
*
* Specifies the resource key for the tasks window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'tasks'.
*/
mxEditor.prototype.tasksResource = (mxClient.language != 'none') ? 'tasks' : '';
/**
* Variable: helpResource
*
* Specifies the resource key for the help window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'help'.
*/
mxEditor.prototype.helpResource = (mxClient.language != 'none') ? 'help' : '';
/**
* Variable: outlineResource
*
* Specifies the resource key for the outline window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'outline'.
*/
mxEditor.prototype.outlineResource = (mxClient.language != 'none') ? 'outline' : '';
/**
* Variable: outline
*
* Reference to the <mxWindow> that contains the outline. The <mxOutline>
* is stored in outline.outline.
*/
mxEditor.prototype.outline = null;
/**
* Variable: graph
*
* Holds a <mxGraph> for displaying the diagram. The graph
* is created in <setGraphContainer>.
*/
mxEditor.prototype.graph = null;
/**
* Variable: graphRenderHint
*
* Holds the render hint used for creating the
* graph in <setGraphContainer>. See <mxGraph>.
* Default is null.
*/
mxEditor.prototype.graphRenderHint = null;
/**
* Variable: toolbar
*
* Holds a <mxDefaultToolbar> for displaying the toolbar. The
* toolbar is created in <setToolbarContainer>.
*/
mxEditor.prototype.toolbar = null;
/**
* Variable: status
*
* DOM container that holds the statusbar. Default is null.
* Use <setStatusContainer> to set this value.
*/
mxEditor.prototype.status = null;
/**
* Variable: popupHandler
*
* Holds a <mxDefaultPopupMenu> for displaying
* popupmenus.
*/
mxEditor.prototype.popupHandler = null;
/**
* Variable: undoManager
*
* Holds an <mxUndoManager> for the command history.
*/
mxEditor.prototype.undoManager = null;
/**
* Variable: keyHandler
*
* Holds a <mxDefaultKeyHandler> for handling keyboard events.
* The handler is created in <setGraphContainer>.
*/
mxEditor.prototype.keyHandler = null;
/**
* Group: Actions and Options
*/
/**
* Variable: actions
*
* Maps from actionnames to actions, which are functions taking
* the editor and the cell as arguments. Use <addAction>
* to add or replace an action and <execute> to execute an action
* by name, passing the cell to be operated upon as the second
* argument.
*/
mxEditor.prototype.actions = null;
/**
* Variable: dblClickAction
*
* Specifies the name of the action to be executed
* when a cell is double clicked. Default is 'edit'.
*
* To handle a singleclick, use the following code.
*
* (code)
* editor.graph.addListener(mxEvent.CLICK, function(sender, evt)
* {
* var e = evt.getProperty('event');
* var cell = evt.getProperty('cell');
*
* if (cell != null && !e.isConsumed())
* {
* // Do something useful with cell...
* e.consume();
* }
* });
* (end)
*/
mxEditor.prototype.dblClickAction = 'edit';
/**
* Variable: swimlaneRequired
*
* Specifies if new cells must be inserted
* into an existing swimlane. Otherwise, cells
* that are not swimlanes can be inserted as
* top-level cells. Default is false.
*/
mxEditor.prototype.swimlaneRequired = false;
/**
* Variable: disableContextMenu
*
* Specifies if the context menu should be disabled in the graph container.
* Default is true.
*/
mxEditor.prototype.disableContextMenu = true;
/**
* Group: Templates
*/
/**
* Variable: insertFunction
*
* Specifies the function to be used for inserting new
* cells into the graph. This is assigned from the
* <mxDefaultToolbar> if a vertex-tool is clicked.
*/
mxEditor.prototype.insertFunction = null;
/**
* Variable: forcedInserting
*
* Specifies if a new cell should be inserted on a single
* click even using <insertFunction> if there is a cell
* under the mousepointer, otherwise the cell under the
* mousepointer is selected. Default is false.
*/
mxEditor.prototype.forcedInserting = false;
/**
* Variable: templates
*
* Maps from names to protoype cells to be used
* in the toolbar for inserting new cells into
* the diagram.
*/
mxEditor.prototype.templates = null;
/**
* Variable: defaultEdge
*
* Prototype edge cell that is used for creating
* new edges.
*/
mxEditor.prototype.defaultEdge = null;
/**
* Variable: defaultEdgeStyle
*
* Specifies the edge style to be returned in <getEdgeStyle>.
* Default is null.
*/
mxEditor.prototype.defaultEdgeStyle = null;
/**
* Variable: defaultGroup
*
* Prototype group cell that is used for creating
* new groups.
*/
mxEditor.prototype.defaultGroup = null;
/**
* Variable: groupBorderSize
*
* Default size for the border of new groups. If null,
* then then <mxGraph.gridSize> is used. Default is
* null.
*/
mxEditor.prototype.groupBorderSize = null;
/**
* Group: Backend Integration
*/
/**
* Variable: filename
*
* Contains the URL of the last opened file as a string.
* Default is null.
*/
mxEditor.prototype.filename = null;
/**
* Variable: lineFeed
*
* Character to be used for encoding linefeeds in <save>. Default is '&#xa;'.
*/
mxEditor.prototype.linefeed = '&#xa;';
/**
* Variable: postParameterName
*
* Specifies if the name of the post parameter that contains the diagram
* data in a post request to the server. Default is 'xml'.
*/
mxEditor.prototype.postParameterName = 'xml';
/**
* Variable: escapePostData
*
* Specifies if the data in the post request for saving a diagram
* should be converted using encodeURIComponent. Default is true.
*/
mxEditor.prototype.escapePostData = true;
/**
* Variable: urlPost
*
* Specifies the URL to be used for posting the diagram
* to a backend in <save>.
*/
mxEditor.prototype.urlPost = null;
/**
* Variable: urlImage
*
* Specifies the URL to be used for creating a bitmap of
* the graph in the image action.
*/
mxEditor.prototype.urlImage = null;
/**
* Group: Autolayout
*/
/**
* Variable: horizontalFlow
*
* Specifies the direction of the flow
* in the diagram. This is used in the
* layout algorithms. Default is false,
* ie. vertical flow.
*/
mxEditor.prototype.horizontalFlow = false;
/**
* Variable: layoutDiagram
*
* Specifies if the top-level elements in the
* diagram should be layed out using a vertical
* or horizontal stack depending on the setting
* of <horizontalFlow>. The spacing between the
* swimlanes is specified by <swimlaneSpacing>.
* Default is false.
*
* If the top-level elements are swimlanes, then
* the intra-swimlane layout is activated by
* the <layoutSwimlanes> switch.
*/
mxEditor.prototype.layoutDiagram = false;
/**
* Variable: swimlaneSpacing
*
* Specifies the spacing between swimlanes if
* automatic layout is turned on in
* <layoutDiagram>. Default is 0.
*/
mxEditor.prototype.swimlaneSpacing = 0;
/**
* Variable: maintainSwimlanes
*
* Specifies if the swimlanes should be kept at the same
* width or height depending on the setting of
* <horizontalFlow>. Default is false.
*
* For horizontal flows, all swimlanes
* have the same height and for vertical flows, all swimlanes
* have the same width. Furthermore, the swimlanes are
* automatically "stacked" if <layoutDiagram> is true.
*/
mxEditor.prototype.maintainSwimlanes = false;
/**
* Variable: layoutSwimlanes
*
* Specifies if the children of swimlanes should
* be layed out, either vertically or horizontally
* depending on <horizontalFlow>.
* Default is false.
*/
mxEditor.prototype.layoutSwimlanes = false;
/**
* Group: Attribute Cycling
*/
/**
* Variable: cycleAttributeValues
*
* Specifies the attribute values to be cycled when
* inserting new swimlanes. Default is an empty
* array.
*/
mxEditor.prototype.cycleAttributeValues = null;
/**
* Variable: cycleAttributeIndex
*
* Index of the last consumed attribute index. If a new
* swimlane is inserted, then the <cycleAttributeValues>
* at this index will be used as the value for
* <cycleAttributeName>. Default is 0.
*/
mxEditor.prototype.cycleAttributeIndex = 0;
/**
* Variable: cycleAttributeName
*
* Name of the attribute to be assigned a <cycleAttributeValues>
* when inserting new swimlanes. Default is 'fillColor'.
*/
mxEditor.prototype.cycleAttributeName = 'fillColor';
/**
* Group: Windows
*/
/**
* Variable: tasks
*
* Holds the <mxWindow> created in <showTasks>.
*/
mxEditor.prototype.tasks = null;
/**
* Variable: tasksWindowImage
*
* Icon for the tasks window.
*/
mxEditor.prototype.tasksWindowImage = null;
/**
* Variable: tasksTop
*
* Specifies the top coordinate of the tasks window in pixels.
* Default is 20.
*/
mxEditor.prototype.tasksTop = 20;
/**
* Variable: help
*
* Holds the <mxWindow> created in <showHelp>.
*/
mxEditor.prototype.help = null;
/**
* Variable: helpWindowImage
*
* Icon for the help window.
*/
mxEditor.prototype.helpWindowImage = null;
/**
* Variable: urlHelp
*
* Specifies the URL to be used for the contents of the
* Online Help window. This is usually specified in the
* resources file under urlHelp for language-specific
* online help support.
*/
mxEditor.prototype.urlHelp = null;
/**
* Variable: helpWidth
*
* Specifies the width of the help window in pixels.
* Default is 300.
*/
mxEditor.prototype.helpWidth = 300;
/**
* Variable: helpHeight
*
* Specifies the height of the help window in pixels.
* Default is 260.
*/
mxEditor.prototype.helpHeight = 260;
/**
* Variable: propertiesWidth
*
* Specifies the width of the properties window in pixels.
* Default is 240.
*/
mxEditor.prototype.propertiesWidth = 240;
/**
* Variable: propertiesHeight
*
* Specifies the height of the properties window in pixels.
* If no height is specified then the window will be automatically
* sized to fit its contents. Default is null.
*/
mxEditor.prototype.propertiesHeight = null;
/**
* Variable: movePropertiesDialog
*
* Specifies if the properties dialog should be automatically
* moved near the cell it is displayed for, otherwise the
* dialog is not moved. This value is only taken into
* account if the dialog is already visible. Default is false.
*/
mxEditor.prototype.movePropertiesDialog = false;
/**
* Variable: validating
*
* Specifies if <mxGraph.validateGraph> should automatically be invoked after
* each change. Default is false.
*/
mxEditor.prototype.validating = false;
/**
* Variable: modified
*
* True if the graph has been modified since it was last saved.
*/
mxEditor.prototype.modified = false;
/**
* Function: isModified
*
* Returns <modified>.
*/
mxEditor.prototype.isModified = function ()
{
return this.modified;
};
/**
* Function: setModified
*
* Sets <modified> to the specified boolean value.
*/
mxEditor.prototype.setModified = function (value)
{
this.modified = value;
};
/**
* Function: addActions
*
* Adds the built-in actions to the editor instance.
*
* save - Saves the graph using <urlPost>.
* print - Shows the graph in a new print preview window.
* show - Shows the graph in a new window.
* exportImage - Shows the graph as a bitmap image using <getUrlImage>.
* refresh - Refreshes the graph's display.
* cut - Copies the current selection into the clipboard
* and removes it from the graph.
* copy - Copies the current selection into the clipboard.
* paste - Pastes the clipboard into the graph.
* delete - Removes the current selection from the graph.
* group - Puts the current selection into a new group.
* ungroup - Removes the selected groups and selects the children.
* undo - Undoes the last change on the graph model.
* redo - Redoes the last change on the graph model.
* zoom - Sets the zoom via a dialog.
* zoomIn - Zooms into the graph.
* zoomOut - Zooms out of the graph
* actualSize - Resets the scale and translation on the graph.
* fit - Changes the scale so that the graph fits into the window.
* showProperties - Shows the properties dialog.
* selectAll - Selects all cells.
* selectNone - Clears the selection.
* selectVertices - Selects all vertices.
* selectEdges = Selects all edges.
* edit - Starts editing the current selection cell.
* enterGroup - Drills down into the current selection cell.
* exitGroup - Moves up in the drilling hierachy
* home - Moves to the topmost parent in the drilling hierarchy
* selectPrevious - Selects the previous cell.
* selectNext - Selects the next cell.
* selectParent - Selects the parent of the selection cell.
* selectChild - Selects the first child of the selection cell.
* collapse - Collapses the currently selected cells.
* expand - Expands the currently selected cells.
* bold - Toggle bold text style.
* italic - Toggle italic text style.
* underline - Toggle underline text style.
* alignCellsLeft - Aligns the selection cells at the left.
* alignCellsCenter - Aligns the selection cells in the center.
* alignCellsRight - Aligns the selection cells at the right.
* alignCellsTop - Aligns the selection cells at the top.
* alignCellsMiddle - Aligns the selection cells in the middle.
* alignCellsBottom - Aligns the selection cells at the bottom.
* alignFontLeft - Sets the horizontal text alignment to left.
* alignFontCenter - Sets the horizontal text alignment to center.
* alignFontRight - Sets the horizontal text alignment to right.
* alignFontTop - Sets the vertical text alignment to top.
* alignFontMiddle - Sets the vertical text alignment to middle.
* alignFontBottom - Sets the vertical text alignment to bottom.
* toggleTasks - Shows or hides the tasks window.
* toggleHelp - Shows or hides the help window.
* toggleOutline - Shows or hides the outline window.
* toggleConsole - Shows or hides the console window.
*/
mxEditor.prototype.addActions = function ()
{
this.addAction('save', function(editor)
{
editor.save();
});
this.addAction('print', function(editor)
{
var preview = new mxPrintPreview(editor.graph, 1);
preview.open();
});
this.addAction('show', function(editor)
{
mxUtils.show(editor.graph, null, 10, 10);
});
this.addAction('exportImage', function(editor)
{
var url = editor.getUrlImage();
if (url == null || mxClient.IS_LOCAL)
{
editor.execute('show');
}
else
{
var node = mxUtils.getViewXml(editor.graph, 1);
var xml = mxUtils.getXml(node, '\n');
mxUtils.submit(url, editor.postParameterName + '=' +
encodeURIComponent(xml), document, '_blank');
}
});
this.addAction('refresh', function(editor)
{
editor.graph.refresh();
});
this.addAction('cut', function(editor)
{
if (editor.graph.isEnabled())
{
mxClipboard.cut(editor.graph);
}
});
this.addAction('copy', function(editor)
{
if (editor.graph.isEnabled())
{
mxClipboard.copy(editor.graph);
}
});
this.addAction('paste', function(editor)
{
if (editor.graph.isEnabled())
{
mxClipboard.paste(editor.graph);
}
});
this.addAction('delete', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.removeCells();
}
});
this.addAction('group', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setSelectionCell(editor.groupCells());
}
});
this.addAction('ungroup', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setSelectionCells(editor.graph.ungroupCells());
}
});
this.addAction('removeFromParent', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.removeCellsFromParent();
}
});
this.addAction('undo', function(editor)
{
if (editor.graph.isEnabled())
{
editor.undo();
}
});
this.addAction('redo', function(editor)
{
if (editor.graph.isEnabled())
{
editor.redo();
}
});
this.addAction('zoomIn', function(editor)
{
editor.graph.zoomIn();
});
this.addAction('zoomOut', function(editor)
{
editor.graph.zoomOut();
});
this.addAction('actualSize', function(editor)
{
editor.graph.zoomActual();
});
this.addAction('fit', function(editor)
{
editor.graph.fit();
});
this.addAction('showProperties', function(editor, cell)
{
editor.showProperties(cell);
});
this.addAction('selectAll', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectAll();
}
});
this.addAction('selectNone', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.clearSelection();
}
});
this.addAction('selectVertices', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectVertices();
}
});
this.addAction('selectEdges', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectEdges();
}
});
this.addAction('edit', function(editor, cell)
{
if (editor.graph.isEnabled() &&
editor.graph.isCellEditable(cell))
{
editor.graph.startEditingAtCell(cell);
}
});
this.addAction('toBack', function(editor, cell)
{
if (editor.graph.isEnabled())
{
editor.graph.orderCells(true);
}
});
this.addAction('toFront', function(editor, cell)
{
if (editor.graph.isEnabled())
{
editor.graph.orderCells(false);
}
});
this.addAction('enterGroup', function(editor, cell)
{
editor.graph.enterGroup(cell);
});
this.addAction('exitGroup', function(editor)
{
editor.graph.exitGroup();
});
this.addAction('home', function(editor)
{
editor.graph.home();
});
this.addAction('selectPrevious', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectPreviousCell();
}
});
this.addAction('selectNext', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectNextCell();
}
});
this.addAction('selectParent', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectParentCell();
}
});
this.addAction('selectChild', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.selectChildCell();
}
});
this.addAction('collapse', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.foldCells(true);
}
});
this.addAction('collapseAll', function(editor)
{
if (editor.graph.isEnabled())
{
var cells = editor.graph.getChildVertices();
editor.graph.foldCells(true, false, cells);
}
});
this.addAction('expand', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.foldCells(false);
}
});
this.addAction('expandAll', function(editor)
{
if (editor.graph.isEnabled())
{
var cells = editor.graph.getChildVertices();
editor.graph.foldCells(false, false, cells);
}
});
this.addAction('bold', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.toggleCellStyleFlags(
mxConstants.STYLE_FONTSTYLE,
mxConstants.FONT_BOLD);
}
});
this.addAction('italic', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.toggleCellStyleFlags(
mxConstants.STYLE_FONTSTYLE,
mxConstants.FONT_ITALIC);
}
});
this.addAction('underline', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.toggleCellStyleFlags(
mxConstants.STYLE_FONTSTYLE,
mxConstants.FONT_UNDERLINE);
}
});
this.addAction('alignCellsLeft', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_LEFT);
}
});
this.addAction('alignCellsCenter', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_CENTER);
}
});
this.addAction('alignCellsRight', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_RIGHT);
}
});
this.addAction('alignCellsTop', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_TOP);
}
});
this.addAction('alignCellsMiddle', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_MIDDLE);
}
});
this.addAction('alignCellsBottom', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.alignCells(mxConstants.ALIGN_BOTTOM);
}
});
this.addAction('alignFontLeft', function(editor)
{
editor.graph.setCellStyles(
mxConstants.STYLE_ALIGN,
mxConstants.ALIGN_LEFT);
});
this.addAction('alignFontCenter', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setCellStyles(
mxConstants.STYLE_ALIGN,
mxConstants.ALIGN_CENTER);
}
});
this.addAction('alignFontRight', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setCellStyles(
mxConstants.STYLE_ALIGN,
mxConstants.ALIGN_RIGHT);
}
});
this.addAction('alignFontTop', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setCellStyles(
mxConstants.STYLE_VERTICAL_ALIGN,
mxConstants.ALIGN_TOP);
}
});
this.addAction('alignFontMiddle', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setCellStyles(
mxConstants.STYLE_VERTICAL_ALIGN,
mxConstants.ALIGN_MIDDLE);
}
});
this.addAction('alignFontBottom', function(editor)
{
if (editor.graph.isEnabled())
{
editor.graph.setCellStyles(
mxConstants.STYLE_VERTICAL_ALIGN,
mxConstants.ALIGN_BOTTOM);
}
});
this.addAction('zoom', function(editor)
{
var current = editor.graph.getView().scale*100;
var scale = parseFloat(mxUtils.prompt(
mxResources.get(editor.askZoomResource) ||
editor.askZoomResource,
current))/100;
if (!isNaN(scale))
{
editor.graph.getView().setScale(scale);
}
});
this.addAction('toggleTasks', function(editor)
{
if (editor.tasks != null)
{
editor.tasks.setVisible(!editor.tasks.isVisible());
}
else
{
editor.showTasks();
}
});
this.addAction('toggleHelp', function(editor)
{
if (editor.help != null)
{
editor.help.setVisible(!editor.help.isVisible());
}
else
{
editor.showHelp();
}
});
this.addAction('toggleOutline', function(editor)
{
if (editor.outline == null)
{
editor.showOutline();
}
else
{
editor.outline.setVisible(!editor.outline.isVisible());
}
});
this.addAction('toggleConsole', function(editor)
{
mxLog.setVisible(!mxLog.isVisible());
});
};
/**
* Function: configure
*
* Configures the editor using the specified node. To load the
* configuration from a given URL the following code can be used to obtain
* the XML node.
*
* (code)
* var node = mxUtils.load(url).getDocumentElement();
* (end)
*
* Parameters:
*
* node - XML node that contains the configuration.
*/
mxEditor.prototype.configure = function (node)
{
if (node != null)
{
// Creates a decoder for the XML data
// and uses it to configure the editor
var dec = new mxCodec(node.ownerDocument);
dec.decode(node, this);
// Resets the counters, modified state and
// command history
this.resetHistory();
}
};
/**
* Function: resetFirstTime
*
* Resets the cookie that is used to remember if the editor has already
* been used.
*/
mxEditor.prototype.resetFirstTime = function ()
{
document.cookie =
'mxgraph=seen; expires=Fri, 27 Jul 2001 02:47:11 UTC; path=/';
};
/**
* Function: resetHistory
*
* Resets the command history, modified state and counters.
*/
mxEditor.prototype.resetHistory = function ()
{
this.lastSnapshot = new Date().getTime();
this.undoManager.clear();
this.ignoredChanges = 0;
this.setModified(false);
};
/**
* Function: addAction
*
* Binds the specified actionname to the specified function.
*
* Parameters:
*
* actionname - String that specifies the name of the action
* to be added.
* funct - Function that implements the new action. The first
* argument of the function is the editor it is used
* with, the second argument is the cell it operates
* upon.
*
* Example:
* (code)
* editor.addAction('test', function(editor, cell)
* {
* mxUtils.alert("test "+cell);
* });
* (end)
*/
mxEditor.prototype.addAction = function (actionname, funct)
{
this.actions[actionname] = funct;
};
/**
* Function: execute
*
* Executes the function with the given name in <actions> passing the
* editor instance and given cell as the first and second argument. All
* additional arguments are passed to the action as well. This method
* contains a try-catch block and displays an error message if an action
* causes an exception. The exception is re-thrown after the error
* message was displayed.
*
* Example:
*
* (code)
* editor.execute("showProperties", cell);
* (end)
*/
mxEditor.prototype.execute = function (actionname, cell, evt)
{
var action = this.actions[actionname];
if (action != null)
{
try
{
// Creates the array of arguments by replacing the actionname
// with the editor instance in the args of this function
var args = arguments;
args[0] = this;
// Invokes the function on the editor using the args
action.apply(this, args);
}
catch (e)
{
mxUtils.error('Cannot execute ' + actionname +
': ' + e.message, 280, true);
throw e;
}
}
else
{
mxUtils.error('Cannot find action '+actionname, 280, true);
}
};
/**
* Function: addTemplate
*
* Adds the specified template under the given name in <templates>.
*/
mxEditor.prototype.addTemplate = function (name, template)
{
this.templates[name] = template;
};
/**
* Function: getTemplate
*
* Returns the template for the given name.
*/
mxEditor.prototype.getTemplate = function (name)
{
return this.templates[name];
};
/**
* Function: createGraph
*
* Creates the <graph> for the editor. The graph is created with no
* container and is initialized from <setGraphContainer>.
*/
mxEditor.prototype.createGraph = function ()
{
var graph = new mxGraph(null, null, this.graphRenderHint);
// Enables rubberband, tooltips, panning
graph.setTooltips(true);
graph.setPanning(true);
// Overrides the dblclick method on the graph to
// invoke the dblClickAction for a cell and reset
// the selection tool in the toolbar
this.installDblClickHandler(graph);
// Installs the command history
this.installUndoHandler(graph);
// Installs the handlers for the root event
this.installDrillHandler(graph);
// Installs the handler for validation
this.installChangeHandler(graph);
// Installs the handler for calling the
// insert function and consume the
// event if an insert function is defined
this.installInsertHandler(graph);
// Redirects the function for creating the
// popupmenu items
graph.popupMenuHandler.factoryMethod =
mxUtils.bind(this, function(menu, cell, evt)
{
return this.createPopupMenu(menu, cell, evt);
});
// Redirects the function for creating
// new connections in the diagram
graph.connectionHandler.factoryMethod =
mxUtils.bind(this, function(source, target)
{
return this.createEdge(source, target);
});
// Maintains swimlanes and installs autolayout
this.createSwimlaneManager(graph);
this.createLayoutManager(graph);
return graph;
};
/**
* Function: createSwimlaneManager
*
* Sets the graph's container using <mxGraph.init>.
*/
mxEditor.prototype.createSwimlaneManager = function (graph)
{
var swimlaneMgr = new mxSwimlaneManager(graph, false);
swimlaneMgr.isHorizontal = mxUtils.bind(this, function()
{
return this.horizontalFlow;
});
swimlaneMgr.isEnabled = mxUtils.bind(this, function()
{
return this.maintainSwimlanes;
});
return swimlaneMgr;
};
/**
* Function: createLayoutManager
*
* Creates a layout manager for the swimlane and diagram layouts, that
* is, the locally defined inter- and intraswimlane layouts.
*/
mxEditor.prototype.createLayoutManager = function (graph)
{
var layoutMgr = new mxLayoutManager(graph);
var self = this; // closure
layoutMgr.getLayout = function(cell)
{
var layout = null;
var model = self.graph.getModel();
if (model.getParent(cell) != null)
{
// Executes the swimlane layout if a child of
// a swimlane has been changed. The layout is
// lazy created in createSwimlaneLayout.
if (self.layoutSwimlanes &&
graph.isSwimlane(cell))
{
if (self.swimlaneLayout == null)
{
self.swimlaneLayout = self.createSwimlaneLayout();
}
layout = self.swimlaneLayout;
}
// Executes the diagram layout if the modified
// cell is a top-level cell. The layout is
// lazy created in createDiagramLayout.
else if (self.layoutDiagram &&
(graph.isValidRoot(cell) ||
model.getParent(model.getParent(cell)) == null))
{
if (self.diagramLayout == null)
{
self.diagramLayout = self.createDiagramLayout();
}
layout = self.diagramLayout;
}
}
return layout;
};
return layoutMgr;
};
/**
* Function: setGraphContainer
*
* Sets the graph's container using <mxGraph.init>.
*/
mxEditor.prototype.setGraphContainer = function (container)
{
if (this.graph.container == null)
{
// Creates the graph instance inside the given container and render hint
//this.graph = new mxGraph(container, null, this.graphRenderHint);
this.graph.init(container);
// Install rubberband selection as the last
// action handler in the chain
this.rubberband = new mxRubberband(this.graph);
// Disables the context menu
if (this.disableContextMenu)
{
mxEvent.disableContextMenu(container);
}
// Workaround for stylesheet directives in IE
if (mxClient.IS_QUIRKS)
{
new mxDivResizer(container);
}
}
};
/**
* Function: installDblClickHandler
*
* Overrides <mxGraph.dblClick> to invoke <dblClickAction>
* on a cell and reset the selection tool in the toolbar.
*/
mxEditor.prototype.installDblClickHandler = function (graph)
{
// Installs a listener for double click events
graph.addListener(mxEvent.DOUBLE_CLICK,
mxUtils.bind(this, function(sender, evt)
{
var cell = evt.getProperty('cell');
if (cell != null &&
graph.isEnabled() &&
this.dblClickAction != null)
{
this.execute(this.dblClickAction, cell);
evt.consume();
}
})
);
};
/**
* Function: installUndoHandler
*
* Adds the <undoManager> to the graph model and the view.
*/
mxEditor.prototype.installUndoHandler = function (graph)
{
var listener = mxUtils.bind(this, function(sender, evt)
{
var edit = evt.getProperty('edit');
this.undoManager.undoableEditHappened(edit);
});
graph.getModel().addListener(mxEvent.UNDO, listener);
graph.getView().addListener(mxEvent.UNDO, listener);
// Keeps the selection state in sync
var undoHandler = function(sender, evt)
{
var changes = evt.getProperty('edit').changes;
graph.setSelectionCells(graph.getSelectionCellsForChanges(changes));
};
this.undoManager.addListener(mxEvent.UNDO, undoHandler);
this.undoManager.addListener(mxEvent.REDO, undoHandler);
};
/**
* Function: installDrillHandler
*
* Installs listeners for dispatching the <root> event.
*/
mxEditor.prototype.installDrillHandler = function (graph)
{
var listener = mxUtils.bind(this, function(sender)
{
this.fireEvent(new mxEventObject(mxEvent.ROOT));
});
graph.getView().addListener(mxEvent.DOWN, listener);
graph.getView().addListener(mxEvent.UP, listener);
};
/**
* Function: installChangeHandler
*
* Installs the listeners required to automatically validate
* the graph. On each change of the root, this implementation
* fires a <root> event.
*/
mxEditor.prototype.installChangeHandler = function (graph)
{
var listener = mxUtils.bind(this, function(sender, evt)
{
// Updates the modified state
this.setModified(true);
// Automatically validates the graph
// after each change
if (this.validating == true)
{
graph.validateGraph();
}
// Checks if the root has been changed
var changes = evt.getProperty('edit').changes;
for (var i = 0; i < changes.length; i++)
{
var change = changes[i];
if (change instanceof mxRootChange ||
(change instanceof mxValueChange &&
change.cell == this.graph.model.root) ||
(change instanceof mxCellAttributeChange &&
change.cell == this.graph.model.root))
{
this.fireEvent(new mxEventObject(mxEvent.ROOT));
break;
}
}
});
graph.getModel().addListener(mxEvent.CHANGE, listener);
};
/**
* Function: installInsertHandler
*
* Installs the handler for invoking <insertFunction> if
* one is defined.
*/
mxEditor.prototype.installInsertHandler = function (graph)
{
var self = this; // closure
var insertHandler =
{
mouseDown: function(sender, me)
{
if (self.insertFunction != null &&
!me.isPopupTrigger() &&
(self.forcedInserting ||
me.getState() == null))
{
self.graph.clearSelection();
self.insertFunction(me.getEvent(), me.getCell());
// Consumes the rest of the events
// for this gesture (down, move, up)
this.isActive = true;
me.consume();
}
},
mouseMove: function(sender, me)
{
if (this.isActive)
{
me.consume();
}
},
mouseUp: function(sender, me)
{
if (this.isActive)
{
this.isActive = false;
me.consume();
}
}
};
graph.addMouseListener(insertHandler);
};
/**
* Function: createDiagramLayout
*
* Creates the layout instance used to layout the
* swimlanes in the diagram.
*/
mxEditor.prototype.createDiagramLayout = function ()
{
var gs = this.graph.gridSize;
var layout = new mxStackLayout(this.graph, !this.horizontalFlow,
this.swimlaneSpacing, 2*gs, 2*gs);
// Overrides isIgnored to only take into account swimlanes
layout.isVertexIgnored = function(cell)
{
return !layout.graph.isSwimlane(cell);
};
return layout;
};
/**
* Function: createSwimlaneLayout
*
* Creates the layout instance used to layout the
* children of each swimlane.
*/
mxEditor.prototype.createSwimlaneLayout = function ()
{
return new mxCompactTreeLayout(this.graph, this.horizontalFlow);
};
/**
* Function: createToolbar
*
* Creates the <toolbar> with no container.
*/
mxEditor.prototype.createToolbar = function ()
{
return new mxDefaultToolbar(null, this);
};
/**
* Function: setToolbarContainer
*
* Initializes the toolbar for the given container.
*/
mxEditor.prototype.setToolbarContainer = function (container)
{
this.toolbar.init(container);
// Workaround for stylesheet directives in IE
if (mxClient.IS_QUIRKS)
{
new mxDivResizer(container);
}
};
/**
* Function: setStatusContainer
*
* Creates the <status> using the specified container.
*
* This implementation adds listeners in the editor to
* display the last saved time and the current filename
* in the status bar.
*
* Parameters:
*
* container - DOM node that will contain the statusbar.
*/
mxEditor.prototype.setStatusContainer = function (container)
{
if (this.status == null)
{
this.status = container;
// Prints the last saved time in the status bar
// when files are saved
this.addListener(mxEvent.SAVE, mxUtils.bind(this, function()
{
var tstamp = new Date().toLocaleString();
this.setStatus((mxResources.get(this.lastSavedResource) ||
this.lastSavedResource)+': '+tstamp);
}));
// Updates the statusbar to display the filename
// when new files are opened
this.addListener(mxEvent.OPEN, mxUtils.bind(this, function()
{
this.setStatus((mxResources.get(this.currentFileResource) ||
this.currentFileResource)+': '+this.filename);
}));
// Workaround for stylesheet directives in IE
if (mxClient.IS_QUIRKS)
{
new mxDivResizer(container);
}
}
};
/**
* Function: setStatus
*
* Display the specified message in the status bar.
*
* Parameters:
*
* message - String the specified the message to
* be displayed.
*/
mxEditor.prototype.setStatus = function (message)
{
if (this.status != null && message != null)
{
this.status.innerHTML = message;
}
};
/**
* Function: setTitleContainer
*
* Creates a listener to update the inner HTML of the
* specified DOM node with the value of <getTitle>.
*
* Parameters:
*
* container - DOM node that will contain the title.
*/
mxEditor.prototype.setTitleContainer = function (container)
{
this.addListener(mxEvent.ROOT, mxUtils.bind(this, function(sender)
{
container.innerHTML = this.getTitle();
}));
// Workaround for stylesheet directives in IE
if (mxClient.IS_QUIRKS)
{
new mxDivResizer(container);
}
};
/**
* Function: treeLayout
*
* Executes a vertical or horizontal compact tree layout
* using the specified cell as an argument. The cell may
* either be a group or the root of a tree.
*
* Parameters:
*
* cell - <mxCell> to use in the compact tree layout.
* horizontal - Optional boolean to specify the tree's
* orientation. Default is true.
*/
mxEditor.prototype.treeLayout = function (cell, horizontal)
{
if (cell != null)
{
var layout = new mxCompactTreeLayout(this.graph, horizontal);
layout.execute(cell);
}
};
/**
* Function: getTitle
*
* Returns the string value for the current root of the
* diagram.
*/
mxEditor.prototype.getTitle = function ()
{
var title = '';
var graph = this.graph;
var cell = graph.getCurrentRoot();
while (cell != null &&
graph.getModel().getParent(
graph.getModel().getParent(cell)) != null)
{
// Append each label of a valid root
if (graph.isValidRoot(cell))
{
title = ' > ' +
graph.convertValueToString(cell) + title;
}
cell = graph.getModel().getParent(cell);
}
var prefix = this.getRootTitle();
return prefix + title;
};
/**
* Function: getRootTitle
*
* Returns the string value of the root cell in
* <mxGraph.model>.
*/
mxEditor.prototype.getRootTitle = function ()
{
var root = this.graph.getModel().getRoot();
return this.graph.convertValueToString(root);
};
/**
* Function: undo
*
* Undo the last change in <graph>.
*/
mxEditor.prototype.undo = function ()
{
this.undoManager.undo();
};
/**
* Function: redo
*
* Redo the last change in <graph>.
*/
mxEditor.prototype.redo = function ()
{
this.undoManager.redo();
};
/**
* Function: groupCells
*
* Invokes <createGroup> to create a new group cell and the invokes
* <mxGraph.groupCells>, using the grid size of the graph as the spacing
* in the group's content area.
*/
mxEditor.prototype.groupCells = function ()
{
var border = (this.groupBorderSize != null) ?
this.groupBorderSize :
this.graph.gridSize;
return this.graph.groupCells(this.createGroup(), border);
};
/**
* Function: createGroup
*
* Creates and returns a clone of <defaultGroup> to be used
* as a new group cell in <group>.
*/
mxEditor.prototype.createGroup = function ()
{
var model = this.graph.getModel();
return model.cloneCell(this.defaultGroup);
};
/**
* Function: open
*
* Opens the specified file synchronously and parses it using
* <readGraphModel>. It updates <filename> and fires an <open>-event after
* the file has been opened. Exceptions should be handled as follows:
*
* (code)
* try
* {
* editor.open(filename);
* }
* catch (e)
* {
* mxUtils.error('Cannot open ' + filename +
* ': ' + e.message, 280, true);
* }
* (end)
*
* Parameters:
*
* filename - URL of the file to be opened.
*/
mxEditor.prototype.open = function (filename)
{
if (filename != null)
{
var xml = mxUtils.load(filename).getXml();
this.readGraphModel(xml.documentElement);
this.filename = filename;
this.fireEvent(new mxEventObject(mxEvent.OPEN, 'filename', filename));
}
};
/**
* Function: readGraphModel
*
* Reads the specified XML node into the existing graph model and resets
* the command history and modified state.
*/
mxEditor.prototype.readGraphModel = function (node)
{
var dec = new mxCodec(node.ownerDocument);
dec.decode(node, this.graph.getModel());
this.resetHistory();
};
/**
* Function: save
*
* Posts the string returned by <writeGraphModel> to the given URL or the
* URL returned by <getUrlPost>. The actual posting is carried out by
* <postDiagram>. If the URL is null then the resulting XML will be
* displayed using <mxUtils.popup>. Exceptions should be handled as
* follows:
*
* (code)
* try
* {
* editor.save();
* }
* catch (e)
* {
* mxUtils.error('Cannot save : ' + e.message, 280, true);
* }
* (end)
*/
mxEditor.prototype.save = function (url, linefeed)
{
// Gets the URL to post the data to
url = url || this.getUrlPost();
// Posts the data if the URL is not empty
if (url != null && url.length > 0)
{
var data = this.writeGraphModel(linefeed);
this.postDiagram(url, data);
// Resets the modified flag
this.setModified(false);
}
// Dispatches a save event
this.fireEvent(new mxEventObject(mxEvent.SAVE, 'url', url));
};
/**
* Function: postDiagram
*
* Hook for subclassers to override the posting of a diagram
* represented by the given node to the given URL. This fires
* an asynchronous <post> event if the diagram has been posted.
*
* Example:
*
* To replace the diagram with the diagram in the response, use the
* following code.
*
* (code)
* editor.addListener(mxEvent.POST, function(sender, evt)
* {
* // Process response (replace diagram)
* var req = evt.getProperty('request');
* var root = req.getDocumentElement();
* editor.graph.readGraphModel(root)
* });
* (end)
*/
mxEditor.prototype.postDiagram = function (url, data)
{
if (this.escapePostData)
{
data = encodeURIComponent(data);
}
mxUtils.post(url, this.postParameterName+'='+data,
mxUtils.bind(this, function(req)
{
this.fireEvent(new mxEventObject(mxEvent.POST,
'request', req, 'url', url, 'data', data));
})
);
};
/**
* Function: writeGraphModel
*
* Hook to create the string representation of the diagram. The default
* implementation uses an <mxCodec> to encode the graph model as
* follows:
*
* (code)
* var enc = new mxCodec();
* var node = enc.encode(this.graph.getModel());
* return mxUtils.getXml(node, this.linefeed);
* (end)
*
* Parameters:
*
* linefeed - Optional character to be used as the linefeed. Default is
* <linefeed>.
*/
mxEditor.prototype.writeGraphModel = function (linefeed)
{
linefeed = (linefeed != null) ? linefeed : this.linefeed;
var enc = new mxCodec();
var node = enc.encode(this.graph.getModel());
return mxUtils.getXml(node, linefeed);
};
/**
* Function: getUrlPost
*
* Returns the URL to post the diagram to. This is used
* in <save>. The default implementation returns <urlPost>,
* adding <code>?draft=true</code>.
*/
mxEditor.prototype.getUrlPost = function ()
{
return this.urlPost;
};
/**
* Function: getUrlImage
*
* Returns the URL to create the image with. This is typically
* the URL of a backend which accepts an XML representation
* of a graph view to create an image. The function is used
* in the image action to create an image. This implementation
* returns <urlImage>.
*/
mxEditor.prototype.getUrlImage = function ()
{
return this.urlImage;
};
/**
* Function: swapStyles
*
* Swaps the styles for the given names in the graph's
* stylesheet and refreshes the graph.
*/
mxEditor.prototype.swapStyles = function (first, second)
{
var style = this.graph.getStylesheet().styles[second];
this.graph.getView().getStylesheet().putCellStyle(
second, this.graph.getStylesheet().styles[first]);
this.graph.getStylesheet().putCellStyle(first, style);
this.graph.refresh();
};
/**
* Function: showProperties
*
* Creates and shows the properties dialog for the given
* cell. The content area of the dialog is created using
* <createProperties>.
*/
mxEditor.prototype.showProperties = function (cell)
{
cell = cell || this.graph.getSelectionCell();
// Uses the root node for the properties dialog
// if not cell was passed in and no cell is
// selected
if (cell == null)
{
cell = this.graph.getCurrentRoot();
if (cell == null)
{
cell = this.graph.getModel().getRoot();
}
}
if (cell != null)
{
// Makes sure there is no in-place editor in the
// graph and computes the location of the dialog
this.graph.stopEditing(true);
var offset = mxUtils.getOffset(this.graph.container);
var x = offset.x+10;
var y = offset.y;
// Avoids moving the dialog if it is alredy open
if (this.properties != null && !this.movePropertiesDialog)
{
x = this.properties.getX();
y = this.properties.getY();
}
// Places the dialog near the cell for which it
// displays the properties
else
{
var bounds = this.graph.getCellBounds(cell);
if (bounds != null)
{
x += bounds.x+Math.min(200, bounds.width);
y += bounds.y;
}
}
// Hides the existing properties dialog and creates a new one with the
// contents created in the hook method
this.hideProperties();
var node = this.createProperties(cell);
if (node != null)
{
// Displays the contents in a window and stores a reference to the
// window for later hiding of the window
this.properties = new mxWindow(mxResources.get(this.propertiesResource) ||
this.propertiesResource, node, x, y, this.propertiesWidth, this.propertiesHeight, false);
this.properties.setVisible(true);
}
}
};
/**
* Function: isPropertiesVisible
*
* Returns true if the properties dialog is currently visible.
*/
mxEditor.prototype.isPropertiesVisible = function ()
{
return this.properties != null;
};
/**
* Function: createProperties
*
* Creates and returns the DOM node that represents the contents
* of the properties dialog for the given cell. This implementation
* works for user objects that are XML nodes and display all the
* node attributes in a form.
*/
mxEditor.prototype.createProperties = function (cell)
{
var model = this.graph.getModel();
var value = model.getValue(cell);
if (mxUtils.isNode(value))
{
// Creates a form for the user object inside
// the cell
var form = new mxForm('properties');
// Adds a readonly field for the cell id
var id = form.addText('ID', cell.getId());
id.setAttribute('readonly', 'true');
var geo = null;
var yField = null;
var xField = null;
var widthField = null;
var heightField = null;
// Adds fields for the location and size
if (model.isVertex(cell))
{
geo = model.getGeometry(cell);
if (geo != null)
{
yField = form.addText('top', geo.y);
xField = form.addText('left', geo.x);
widthField = form.addText('width', geo.width);
heightField = form.addText('height', geo.height);
}
}
// Adds a field for the cell style
var tmp = model.getStyle(cell);
var style = form.addText('Style', tmp || '');
// Creates textareas for each attribute of the
// user object within the cell
var attrs = value.attributes;
var texts = [];
for (var i = 0; i < attrs.length; i++)
{
// Creates a textarea with more lines for
// the cell label
var val = attrs[i].value;
texts[i] = form.addTextarea(attrs[i].nodeName, val,
(attrs[i].nodeName == 'label') ? 4 : 2);
}
// Adds an OK and Cancel button to the dialog
// contents and implements the respective
// actions below
// Defines the function to be executed when the
// OK button is pressed in the dialog
var okFunction = mxUtils.bind(this, function()
{
// Hides the dialog
this.hideProperties();
// Supports undo for the changes on the underlying
// XML structure / XML node attribute changes.
model.beginUpdate();
try
{
if (geo != null)
{
geo = geo.clone();
geo.x = parseFloat(xField.value);
geo.y = parseFloat(yField.value);
geo.width = parseFloat(widthField.value);
geo.height = parseFloat(heightField.value);
model.setGeometry(cell, geo);
}
// Applies the style
if (style.value.length > 0)
{
model.setStyle(cell, style.value);
}
else
{
model.setStyle(cell, null);
}
// Creates an undoable change for each
// attribute and executes it using the
// model, which will also make the change
// part of the current transaction
for (var i=0; i<attrs.length; i++)
{
var edit = new mxCellAttributeChange(
cell, attrs[i].nodeName,
texts[i].value);
model.execute(edit);
}
// Checks if the graph wants cells to
// be automatically sized and updates
// the size as an undoable step if
// the feature is enabled
if (this.graph.isAutoSizeCell(cell))
{
this.graph.updateCellSize(cell);
}
}
finally
{
model.endUpdate();
}
});
// Defines the function to be executed when the
// Cancel button is pressed in the dialog
var cancelFunction = mxUtils.bind(this, function()
{
// Hides the dialog
this.hideProperties();
});
form.addButtons(okFunction, cancelFunction);
return form.table;
}
return null;
};
/**
* Function: hideProperties
*
* Hides the properties dialog.
*/
mxEditor.prototype.hideProperties = function ()
{
if (this.properties != null)
{
this.properties.destroy();
this.properties = null;
}
};
/**
* Function: showTasks
*
* Shows the tasks window. The tasks window is created using <createTasks>. The
* default width of the window is 200 pixels, the y-coordinate of the location
* can be specifies in <tasksTop> and the x-coordinate is right aligned with a
* 20 pixel offset from the right border. To change the location of the tasks
* window, the following code can be used:
*
* (code)
* var oldShowTasks = mxEditor.prototype.showTasks;
* mxEditor.prototype.showTasks = function()
* {
* oldShowTasks.apply(this, arguments); // "supercall"
*
* if (this.tasks != null)
* {
* this.tasks.setLocation(10, 10);
* }
* };
* (end)
*/
mxEditor.prototype.showTasks = function ()
{
if (this.tasks == null)
{
var div = document.createElement('div');
div.style.padding = '4px';
div.style.paddingLeft = '20px';
var w = document.body.clientWidth;
var wnd = new mxWindow(
mxResources.get(this.tasksResource) ||
this.tasksResource,
div, w - 220, this.tasksTop, 200);
wnd.setClosable(true);
wnd.destroyOnClose = false;
// Installs a function to update the contents
// of the tasks window on every change of the
// model, selection or root.
var funct = mxUtils.bind(this, function(sender)
{
mxEvent.release(div);
div.innerHTML = '';
this.createTasks(div);
});
this.graph.getModel().addListener(mxEvent.CHANGE, funct);
this.graph.getSelectionModel().addListener(mxEvent.CHANGE, funct);
this.graph.addListener(mxEvent.ROOT, funct);
// Assigns the icon to the tasks window
if (this.tasksWindowImage != null)
{
wnd.setImage(this.tasksWindowImage);
}
this.tasks = wnd;
this.createTasks(div);
}
this.tasks.setVisible(true);
};
/**
* Function: refreshTasks
*
* Updates the contents of the tasks window using <createTasks>.
*/
mxEditor.prototype.refreshTasks = function (div)
{
if (this.tasks != null)
{
var div = this.tasks.content;
mxEvent.release(div);
div.innerHTML = '';
this.createTasks(div);
}
};
/**
* Function: createTasks
*
* Updates the contents of the given DOM node to
* display the tasks associated with the current
* editor state. This is invoked whenever there
* is a possible change of state in the editor.
* Default implementation is empty.
*/
mxEditor.prototype.createTasks = function (div)
{
// override
};
/**
* Function: showHelp
*
* Shows the help window. If the help window does not exist
* then it is created using an iframe pointing to the resource
* for the <code>urlHelp</code> key or <urlHelp> if the resource
* is undefined.
*/
mxEditor.prototype.showHelp = function (tasks)
{
if (this.help == null)
{
var frame = document.createElement('iframe');
frame.setAttribute('src', mxResources.get('urlHelp') || this.urlHelp);
frame.setAttribute('height', '100%');
frame.setAttribute('width', '100%');
frame.setAttribute('frameBorder', '0');
frame.style.backgroundColor = 'white';
var w = document.body.clientWidth;
var h = (document.body.clientHeight || document.documentElement.clientHeight);
var wnd = new mxWindow(mxResources.get(this.helpResource) || this.helpResource,
frame, (w-this.helpWidth)/2, (h-this.helpHeight)/3, this.helpWidth, this.helpHeight);
wnd.setMaximizable(true);
wnd.setClosable(true);
wnd.destroyOnClose = false;
wnd.setResizable(true);
// Assigns the icon to the help window
if (this.helpWindowImage != null)
{
wnd.setImage(this.helpWindowImage);
}
// Workaround for ignored iframe height 100% in FF
if (mxClient.IS_NS)
{
var handler = function(sender)
{
var h = wnd.div.offsetHeight;
frame.setAttribute('height', (h-26)+'px');
};
wnd.addListener(mxEvent.RESIZE_END, handler);
wnd.addListener(mxEvent.MAXIMIZE, handler);
wnd.addListener(mxEvent.NORMALIZE, handler);
wnd.addListener(mxEvent.SHOW, handler);
}
this.help = wnd;
}
this.help.setVisible(true);
};
/**
* Function: showOutline
*
* Shows the outline window. If the window does not exist, then it is
* created using an <mxOutline>.
*/
mxEditor.prototype.showOutline = function ()
{
var create = this.outline == null;
if (create)
{
var div = document.createElement('div');
div.style.overflow = 'hidden';
div.style.position = 'relative';
div.style.width = '100%';
div.style.height = '100%';
div.style.background = 'white';
div.style.cursor = 'move';
if (document.documentMode == 8)
{
div.style.filter = 'progid:DXImageTransform.Microsoft.alpha(opacity=100)';
}
var wnd = new mxWindow(
mxResources.get(this.outlineResource) ||
this.outlineResource,
div, 600, 480, 200, 200, false);
// Creates the outline in the specified div
// and links it to the existing graph
var outline = new mxOutline(this.graph, div);
wnd.setClosable(true);
wnd.setResizable(true);
wnd.destroyOnClose = false;
wnd.addListener(mxEvent.RESIZE_END, function()
{
outline.update();
});
this.outline = wnd;
this.outline.outline = outline;
}
// Finally shows the outline
this.outline.setVisible(true);
this.outline.outline.update(true);
};
/**
* Function: setMode
*
* Puts the graph into the specified mode. The following modenames are
* supported:
*
* select - Selects using the left mouse button, new connections
* are disabled.
* connect - Selects using the left mouse button or creates new
* connections if mouse over cell hotspot. See <mxConnectionHandler>.
* pan - Pans using the left mouse button, new connections are disabled.
*/
mxEditor.prototype.setMode = function(modename)
{
if (modename == 'select')
{
this.graph.panningHandler.useLeftButtonForPanning = false;
this.graph.setConnectable(false);
}
else if (modename == 'connect')
{
this.graph.panningHandler.useLeftButtonForPanning = false;
this.graph.setConnectable(true);
}
else if (modename == 'pan')
{
this.graph.panningHandler.useLeftButtonForPanning = true;
this.graph.setConnectable(false);
}
};
/**
* Function: createPopupMenu
*
* Uses <popupHandler> to create the menu in the graph's
* panning handler. The redirection is setup in
* <setToolbarContainer>.
*/
mxEditor.prototype.createPopupMenu = function (menu, cell, evt)
{
this.popupHandler.createMenu(this, menu, cell, evt);
};
/**
* Function: createEdge
*
* Uses <defaultEdge> as the prototype for creating new edges
* in the connection handler of the graph. The style of the
* edge will be overridden with the value returned by
* <getEdgeStyle>.
*/
mxEditor.prototype.createEdge = function (source, target)
{
// Clones the defaultedge prototype
var e = null;
if (this.defaultEdge != null)
{
var model = this.graph.getModel();
e = model.cloneCell(this.defaultEdge);
}
else
{
e = new mxCell('');
e.setEdge(true);
var geo = new mxGeometry();
geo.relative = true;
e.setGeometry(geo);
}
// Overrides the edge style
var style = this.getEdgeStyle();
if (style != null)
{
e.setStyle(style);
}
return e;
};
/**
* Function: getEdgeStyle
*
* Returns a string identifying the style of new edges.
* The function is used in <createEdge> when new edges
* are created in the graph.
*/
mxEditor.prototype.getEdgeStyle = function ()
{
return this.defaultEdgeStyle;
};
/**
* Function: consumeCycleAttribute
*
* Returns the next attribute in <cycleAttributeValues>
* or null, if not attribute should be used in the
* specified cell.
*/
mxEditor.prototype.consumeCycleAttribute = function (cell)
{
return (this.cycleAttributeValues != null &&
this.cycleAttributeValues.length > 0 &&
this.graph.isSwimlane(cell)) ?
this.cycleAttributeValues[this.cycleAttributeIndex++ %
this.cycleAttributeValues.length] : null;
};
/**
* Function: cycleAttribute
*
* Uses the returned value from <consumeCycleAttribute>
* as the value for the <cycleAttributeName> key in
* the given cell's style.
*/
mxEditor.prototype.cycleAttribute = function (cell)
{
if (this.cycleAttributeName != null)
{
var value = this.consumeCycleAttribute(cell);
if (value != null)
{
cell.setStyle(cell.getStyle()+';'+
this.cycleAttributeName+'='+value);
}
}
};
/**
* Function: addVertex
*
* Adds the given vertex as a child of parent at the specified
* x and y coordinate and fires an <addVertex> event.
*/
mxEditor.prototype.addVertex = function (parent, vertex, x, y)
{
var model = this.graph.getModel();
while (parent != null && !this.graph.isValidDropTarget(parent))
{
parent = model.getParent(parent);
}
parent = (parent != null) ? parent : this.graph.getSwimlaneAt(x, y);
var scale = this.graph.getView().scale;
var geo = model.getGeometry(vertex);
var pgeo = model.getGeometry(parent);
if (this.graph.isSwimlane(vertex) &&
!this.graph.swimlaneNesting)
{
parent = null;
}
else if (parent == null && this.swimlaneRequired)
{
return null;
}
else if (parent != null && pgeo != null)
{
// Keeps vertex inside parent
var state = this.graph.getView().getState(parent);
if (state != null)
{
x -= state.origin.x * scale;
y -= state.origin.y * scale;
if (this.graph.isConstrainedMoving)
{
var width = geo.width;
var height = geo.height;
var tmp = state.x+state.width;
if (x+width > tmp)
{
x -= x+width - tmp;
}
tmp = state.y+state.height;
if (y+height > tmp)
{
y -= y+height - tmp;
}
}
}
else if (pgeo != null)
{
x -= pgeo.x*scale;
y -= pgeo.y*scale;
}
}
geo = geo.clone();
geo.x = this.graph.snap(x / scale -
this.graph.getView().translate.x -
this.graph.gridSize/2);
geo.y = this.graph.snap(y / scale -
this.graph.getView().translate.y -
this.graph.gridSize/2);
vertex.setGeometry(geo);
if (parent == null)
{
parent = this.graph.getDefaultParent();
}
this.cycleAttribute(vertex);
this.fireEvent(new mxEventObject(mxEvent.BEFORE_ADD_VERTEX,
'vertex', vertex, 'parent', parent));
model.beginUpdate();
try
{
vertex = this.graph.addCell(vertex, parent);
if (vertex != null)
{
this.graph.constrainChild(vertex);
this.fireEvent(new mxEventObject(mxEvent.ADD_VERTEX, 'vertex', vertex));
}
}
finally
{
model.endUpdate();
}
if (vertex != null)
{
this.graph.setSelectionCell(vertex);
this.graph.scrollCellToVisible(vertex);
this.fireEvent(new mxEventObject(mxEvent.AFTER_ADD_VERTEX, 'vertex', vertex));
}
return vertex;
};
/**
* Function: destroy
*
* Removes the editor and all its associated resources. This does not
* normally need to be called, it is called automatically when the window
* unloads.
*/
mxEditor.prototype.destroy = function ()
{
if (!this.destroyed)
{
this.destroyed = true;
if (this.tasks != null)
{
this.tasks.destroy();
}
if (this.outline != null)
{
this.outline.destroy();
}
if (this.properties != null)
{
this.properties.destroy();
}
if (this.keyHandler != null)
{
this.keyHandler.destroy();
}
if (this.rubberband != null)
{
this.rubberband.destroy();
}
if (this.toolbar != null)
{
this.toolbar.destroy();
}
if (this.graph != null)
{
this.graph.destroy();
}
this.status = null;
this.templates = null;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxCodecRegistry =
{
/**
* Class: mxCodecRegistry
*
* Singleton class that acts as a global registry for codecs.
*
* Adding an <mxCodec>:
*
* 1. Define a default codec with a new instance of the
* object to be handled.
*
* (code)
* var codec = new mxObjectCodec(new mxGraphModel());
* (end)
*
* 2. Define the functions required for encoding and decoding
* objects.
*
* (code)
* codec.encode = function(enc, obj) { ... }
* codec.decode = function(dec, node, into) { ... }
* (end)
*
* 3. Register the codec in the <mxCodecRegistry>.
*
* (code)
* mxCodecRegistry.register(codec);
* (end)
*
* <mxObjectCodec.decode> may be used to either create a new
* instance of an object or to configure an existing instance,
* in which case the into argument points to the existing
* object. In this case, we say the codec "configures" the
* object.
*
* Variable: codecs
*
* Maps from constructor names to codecs.
*/
codecs: [],
/**
* Variable: aliases
*
* Maps from classnames to codecnames.
*/
aliases: [],
/**
* Function: register
*
* Registers a new codec and associates the name of the template
* constructor in the codec with the codec object.
*
* Parameters:
*
* codec - <mxObjectCodec> to be registered.
*/
register: function(codec)
{
if (codec != null)
{
var name = codec.getName();
mxCodecRegistry.codecs[name] = codec;
var classname = mxUtils.getFunctionName(codec.template.constructor);
if (classname != name)
{
mxCodecRegistry.addAlias(classname, name);
}
}
return codec;
},
/**
* Function: addAlias
*
* Adds an alias for mapping a classname to a codecname.
*/
addAlias: function(classname, codecname)
{
mxCodecRegistry.aliases[classname] = codecname;
},
/**
* Function: getCodec
*
* Returns a codec that handles objects that are constructed
* using the given constructor.
*
* Parameters:
*
* ctor - JavaScript constructor function.
*/
getCodec: function(ctor)
{
var codec = null;
if (ctor != null)
{
var name = mxUtils.getFunctionName(ctor);
var tmp = mxCodecRegistry.aliases[name];
if (tmp != null)
{
name = tmp;
}
codec = mxCodecRegistry.codecs[name];
// Registers a new default codec for the given constructor
// if no codec has been previously defined.
if (codec == null)
{
try
{
codec = new mxObjectCodec(new ctor());
mxCodecRegistry.register(codec);
}
catch (e)
{
// ignore
}
}
}
return codec;
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxCodec
*
* XML codec for JavaScript object graphs. See <mxObjectCodec> for a
* description of the general encoding/decoding scheme. This class uses the
* codecs registered in <mxCodecRegistry> for encoding/decoding each object.
*
* References:
*
* In order to resolve references, especially forward references, the mxCodec
* constructor must be given the document that contains the referenced
* elements.
*
* Examples:
*
* The following code is used to encode a graph model.
*
* (code)
* var encoder = new mxCodec();
* var result = encoder.encode(graph.getModel());
* var xml = mxUtils.getXml(result);
* (end)
*
* Example:
*
* Using the code below, an XML document is decoded into an existing model. The
* document may be obtained using one of the functions in mxUtils for loading
* an XML file, eg. <mxUtils.get>, or using <mxUtils.parseXml> for parsing an
* XML string.
*
* (code)
* var doc = mxUtils.parseXml(xmlString);
* var codec = new mxCodec(doc);
* codec.decode(doc.documentElement, graph.getModel());
* (end)
*
* Example:
*
* This example demonstrates parsing a list of isolated cells into an existing
* graph model. Note that the cells do not have a parent reference so they can
* be added anywhere in the cell hierarchy after parsing.
*
* (code)
* var xml = '<root><mxCell id="2" value="Hello," vertex="1"><mxGeometry x="20" y="20" width="80" height="30" as="geometry"/></mxCell><mxCell id="3" value="World!" vertex="1"><mxGeometry x="200" y="150" width="80" height="30" as="geometry"/></mxCell><mxCell id="4" value="" edge="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell></root>';
* var doc = mxUtils.parseXml(xml);
* var codec = new mxCodec(doc);
* var elt = doc.documentElement.firstChild;
* var cells = [];
*
* while (elt != null)
* {
* cells.push(codec.decode(elt));
* elt = elt.nextSibling;
* }
*
* graph.addCells(cells);
* (end)
*
* Example:
*
* Using the following code, the selection cells of a graph are encoded and the
* output is displayed in a dialog box.
*
* (code)
* var enc = new mxCodec();
* var cells = graph.getSelectionCells();
* mxUtils.alert(mxUtils.getPrettyXml(enc.encode(cells)));
* (end)
*
* Newlines in the XML can be converted to <br>, in which case a '<br>' argument
* must be passed to <mxUtils.getXml> as the second argument.
*
* Debugging:
*
* For debugging I/O you can use the following code to get the sequence of
* encoded objects:
*
* (code)
* var oldEncode = mxCodec.prototype.encode;
* mxCodec.prototype.encode = function(obj)
* {
* mxLog.show();
* mxLog.debug('mxCodec.encode: obj='+mxUtils.getFunctionName(obj.constructor));
*
* return oldEncode.apply(this, arguments);
* };
* (end)
*
* Note that the I/O system adds object codecs for new object automatically. For
* decoding those objects, the constructor should be written as follows:
*
* (code)
* var MyObj = function(name)
* {
* // ...
* };
* (end)
*
* Constructor: mxCodec
*
* Constructs an XML encoder/decoder for the specified
* owner document.
*
* Parameters:
*
* document - Optional XML document that contains the data.
* If no document is specified then a new document is created
* using <mxUtils.createXmlDocument>.
*/
function mxCodec(document)
{
this.document = document || mxUtils.createXmlDocument();
this.objects = [];
};
/**
* Variable: document
*
* The owner document of the codec.
*/
mxCodec.prototype.document = null;
/**
* Variable: objects
*
* Maps from IDs to objects.
*/
mxCodec.prototype.objects = null;
/**
* Variable: elements
*
* Lookup table for resolving IDs to elements.
*/
mxCodec.prototype.elements = null;
/**
* Variable: encodeDefaults
*
* Specifies if default values should be encoded. Default is false.
*/
mxCodec.prototype.encodeDefaults = false;
/**
* Function: putObject
*
* Assoiates the given object with the given ID and returns the given object.
*
* Parameters
*
* id - ID for the object to be associated with.
* obj - Object to be associated with the ID.
*/
mxCodec.prototype.putObject = function(id, obj)
{
this.objects[id] = obj;
return obj;
};
/**
* Function: getObject
*
* Returns the decoded object for the element with the specified ID in
* <document>. If the object is not known then <lookup> is used to find an
* object. If no object is found, then the element with the respective ID
* from the document is parsed using <decode>.
*/
mxCodec.prototype.getObject = function(id)
{
var obj = null;
if (id != null)
{
obj = this.objects[id];
if (obj == null)
{
obj = this.lookup(id);
if (obj == null)
{
var node = this.getElementById(id);
if (node != null)
{
obj = this.decode(node);
}
}
}
}
return obj;
};
/**
* Function: lookup
*
* Hook for subclassers to implement a custom lookup mechanism for cell IDs.
* This implementation always returns null.
*
* Example:
*
* (code)
* var codec = new mxCodec();
* codec.lookup = function(id)
* {
* return model.getCell(id);
* };
* (end)
*
* Parameters:
*
* id - ID of the object to be returned.
*/
mxCodec.prototype.lookup = function(id)
{
return null;
};
/**
* Function: getElementById
*
* Returns the element with the given ID from <document>.
*
* Parameters:
*
* id - String that contains the ID.
*/
mxCodec.prototype.getElementById = function(id)
{
this.updateElements();
return this.elements[id];
};
/**
* Function: updateElements
*
* Returns the element with the given ID from <document>.
*
* Parameters:
*
* id - String that contains the ID.
*/
mxCodec.prototype.updateElements = function()
{
if (this.elements == null)
{
this.elements = new Object();
if (this.document.documentElement != null)
{
this.addElement(this.document.documentElement);
}
}
};
/**
* Function: addElement
*
* Adds the given element to <elements> if it has an ID.
*/
mxCodec.prototype.addElement = function(node)
{
if (node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var id = node.getAttribute('id');
if (id != null)
{
if (this.elements[id] == null)
{
this.elements[id] = node;
}
else if (this.elements[id] != node)
{
throw new Error(id + ': Duplicate ID');
}
}
}
node = node.firstChild;
while (node != null)
{
this.addElement(node);
node = node.nextSibling;
}
};
/**
* Function: getId
*
* Returns the ID of the specified object. This implementation
* calls <reference> first and if that returns null handles
* the object as an <mxCell> by returning their IDs using
* <mxCell.getId>. If no ID exists for the given cell, then
* an on-the-fly ID is generated using <mxCellPath.create>.
*
* Parameters:
*
* obj - Object to return the ID for.
*/
mxCodec.prototype.getId = function(obj)
{
var id = null;
if (obj != null)
{
id = this.reference(obj);
if (id == null && obj instanceof mxCell)
{
id = obj.getId();
if (id == null)
{
// Uses an on-the-fly Id
id = mxCellPath.create(obj);
if (id.length == 0)
{
id = 'root';
}
}
}
}
return id;
};
/**
* Function: reference
*
* Hook for subclassers to implement a custom method
* for retrieving IDs from objects. This implementation
* always returns null.
*
* Example:
*
* (code)
* var codec = new mxCodec();
* codec.reference = function(obj)
* {
* return obj.getCustomId();
* };
* (end)
*
* Parameters:
*
* obj - Object whose ID should be returned.
*/
mxCodec.prototype.reference = function(obj)
{
return null;
};
/**
* Function: encode
*
* Encodes the specified object and returns the resulting
* XML node.
*
* Parameters:
*
* obj - Object to be encoded.
*/
mxCodec.prototype.encode = function(obj)
{
var node = null;
if (obj != null && obj.constructor != null)
{
var enc = mxCodecRegistry.getCodec(obj.constructor);
if (enc != null)
{
node = enc.encode(this, obj);
}
else
{
if (mxUtils.isNode(obj))
{
node = mxUtils.importNode(this.document, obj, true);
}
else
{
mxLog.warn('mxCodec.encode: No codec for ' + mxUtils.getFunctionName(obj.constructor));
}
}
}
return node;
};
/**
* Function: decode
*
* Decodes the given XML node. The optional "into"
* argument specifies an existing object to be
* used. If no object is given, then a new instance
* is created using the constructor from the codec.
*
* The function returns the passed in object or
* the new instance if no object was given.
*
* Parameters:
*
* node - XML node to be decoded.
* into - Optional object to be decodec into.
*/
mxCodec.prototype.decode = function(node, into)
{
this.updateElements();
var obj = null;
if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var ctor = null;
try
{
ctor = window[node.nodeName];
}
catch (err)
{
// ignore
}
var dec = mxCodecRegistry.getCodec(ctor);
if (dec != null)
{
obj = dec.decode(this, node, into);
}
else
{
obj = node.cloneNode(true);
obj.removeAttribute('as');
}
}
return obj;
};
/**
* Function: encodeCell
*
* Encoding of cell hierarchies is built-into the core, but
* is a higher-level function that needs to be explicitely
* used by the respective object encoders (eg. <mxModelCodec>,
* <mxChildChangeCodec> and <mxRootChangeCodec>). This
* implementation writes the given cell and its children as a
* (flat) sequence into the given node. The children are not
* encoded if the optional includeChildren is false. The
* function is in charge of adding the result into the
* given node and has no return value.
*
* Parameters:
*
* cell - <mxCell> to be encoded.
* node - Parent XML node to add the encoded cell into.
* includeChildren - Optional boolean indicating if the
* function should include all descendents. Default is true.
*/
mxCodec.prototype.encodeCell = function(cell, node, includeChildren)
{
node.appendChild(this.encode(cell));
if (includeChildren == null || includeChildren)
{
var childCount = cell.getChildCount();
for (var i = 0; i < childCount; i++)
{
this.encodeCell(cell.getChildAt(i), node);
}
}
};
/**
* Function: isCellCodec
*
* Returns true if the given codec is a cell codec. This uses
* <mxCellCodec.isCellCodec> to check if the codec is of the
* given type.
*/
mxCodec.prototype.isCellCodec = function(codec)
{
if (codec != null && typeof(codec.isCellCodec) == 'function')
{
return codec.isCellCodec();
}
return false;
};
/**
* Function: decodeCell
*
* Decodes cells that have been encoded using inversion, ie.
* where the user object is the enclosing node in the XML,
* and restores the group and graph structure in the cells.
* Returns a new <mxCell> instance that represents the
* given node.
*
* Parameters:
*
* node - XML node that contains the cell data.
* restoreStructures - Optional boolean indicating whether
* the graph structure should be restored by calling insert
* and insertEdge on the parent and terminals, respectively.
* Default is true.
*/
mxCodec.prototype.decodeCell = function(node, restoreStructures)
{
restoreStructures = (restoreStructures != null) ? restoreStructures : true;
var cell = null;
if (node != null && node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Tries to find a codec for the given node name. If that does
// not return a codec then the node is the user object (an XML node
// that contains the mxCell, aka inversion).
var decoder = mxCodecRegistry.getCodec(node.nodeName);
// Tries to find the codec for the cell inside the user object.
// This assumes all node names inside the user object are either
// not registered or they correspond to a class for cells.
if (!this.isCellCodec(decoder))
{
var child = node.firstChild;
while (child != null && !this.isCellCodec(decoder))
{
decoder = mxCodecRegistry.getCodec(child.nodeName);
child = child.nextSibling;
}
}
if (!this.isCellCodec(decoder))
{
decoder = mxCodecRegistry.getCodec(mxCell);
}
cell = decoder.decode(this, node);
if (restoreStructures)
{
this.insertIntoGraph(cell);
}
}
return cell;
};
/**
* Function: insertIntoGraph
*
* Inserts the given cell into its parent and terminal cells.
*/
mxCodec.prototype.insertIntoGraph = function(cell)
{
var parent = cell.parent;
var source = cell.getTerminal(true);
var target = cell.getTerminal(false);
// Fixes possible inconsistencies during insert into graph
cell.setTerminal(null, false);
cell.setTerminal(null, true);
cell.parent = null;
if (parent != null)
{
if (parent == cell)
{
throw new Error(parent.id + ': Self Reference');
}
else
{
parent.insert(cell);
}
}
if (source != null)
{
source.insertEdge(cell, true);
}
if (target != null)
{
target.insertEdge(cell, false);
}
};
/**
* Function: setAttribute
*
* Sets the attribute on the specified node to value. This is a
* helper method that makes sure the attribute and value arguments
* are not null.
*
* Parameters:
*
* node - XML node to set the attribute for.
* attributes - Attributename to be set.
* value - New value of the attribute.
*/
mxCodec.prototype.setAttribute = function(node, attribute, value)
{
if (attribute != null && value != null)
{
node.setAttribute(attribute, value);
}
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxObjectCodec
*
* Generic codec for JavaScript objects that implements a mapping between
* JavaScript objects and XML nodes that maps each field or element to an
* attribute or child node, and vice versa.
*
* Atomic Values:
*
* Consider the following example.
*
* (code)
* var obj = new Object();
* obj.foo = "Foo";
* obj.bar = "Bar";
* (end)
*
* This object is encoded into an XML node using the following.
*
* (code)
* var enc = new mxCodec();
* var node = enc.encode(obj);
* (end)
*
* The output of the encoding may be viewed using <mxLog> as follows.
*
* (code)
* mxLog.show();
* mxLog.debug(mxUtils.getPrettyXml(node));
* (end)
*
* Finally, the result of the encoding looks as follows.
*
* (code)
* <Object foo="Foo" bar="Bar"/>
* (end)
*
* In the above output, the foo and bar fields have been mapped to attributes
* with the same names, and the name of the constructor was used for the
* nodename.
*
* Booleans:
*
* Since booleans are numbers in JavaScript, all boolean values are encoded
* into 1 for true and 0 for false. The decoder also accepts the string true
* and false for boolean values.
*
* Objects:
*
* The above scheme is applied to all atomic fields, that is, to all non-object
* fields of an object. For object fields, a child node is created with a
* special attribute that contains the fieldname. This special attribute is
* called "as" and hence, as is a reserved word that should not be used for a
* fieldname.
*
* Consider the following example where foo is an object and bar is an atomic
* property of foo.
*
* (code)
* var obj = {foo: {bar: "Bar"}};
* (end)
*
* This will be mapped to the following XML structure by mxObjectCodec.
*
* (code)
* <Object>
* <Object bar="Bar" as="foo"/>
* </Object>
* (end)
*
* In the above output, the inner Object node contains the as-attribute that
* specifies the fieldname in the enclosing object. That is, the field foo was
* mapped to a child node with an as-attribute that has the value foo.
*
* Arrays:
*
* Arrays are special objects that are either associative, in which case each
* key, value pair is treated like a field where the key is the fieldname, or
* they are a sequence of atomic values and objects, which is mapped to a
* sequence of child nodes. For object elements, the above scheme is applied
* without the use of the special as-attribute for creating each child. For
* atomic elements, a special add-node is created with the value stored in the
* value-attribute.
*
* For example, the following array contains one atomic value and one object
* with a field called bar. Furthermore it contains two associative entries
* called bar with an atomic value, and foo with an object value.
*
* (code)
* var obj = ["Bar", {bar: "Bar"}];
* obj["bar"] = "Bar";
* obj["foo"] = {bar: "Bar"};
* (end)
*
* This array is represented by the following XML nodes.
*
* (code)
* <Array bar="Bar">
* <add value="Bar"/>
* <Object bar="Bar"/>
* <Object bar="Bar" as="foo"/>
* </Array>
* (end)
*
* The Array node name is the name of the constructor. The additional
* as-attribute in the last child contains the key of the associative entry,
* whereas the second last child is part of the array sequence and does not
* have an as-attribute.
*
* References:
*
* Objects may be represented as child nodes or attributes with ID values,
* which are used to lookup the object in a table within <mxCodec>. The
* <isReference> function is in charge of deciding if a specific field should
* be encoded as a reference or not. Its default implementation returns true if
* the fieldname is in <idrefs>, an array of strings that is used to configure
* the <mxObjectCodec>.
*
* Using this approach, the mapping does not guarantee that the referenced
* object itself exists in the document. The fields that are encoded as
* references must be carefully chosen to make sure all referenced objects
* exist in the document, or may be resolved by some other means if necessary.
*
* For example, in the case of the graph model all cells are stored in a tree
* whose root is referenced by the model's root field. A tree is a structure
* that is well suited for an XML representation, however, the additional edges
* in the graph model have a reference to a source and target cell, which are
* also contained in the tree. To handle this case, the source and target cell
* of an edge are treated as references, whereas the children are treated as
* objects. Since all cells are contained in the tree and no edge references a
* source or target outside the tree, this setup makes sure all referenced
* objects are contained in the document.
*
* In the case of a tree structure we must further avoid infinite recursion by
* ignoring the parent reference of each child. This is done by returning true
* in <isExcluded>, whose default implementation uses the array of excluded
* fieldnames passed to the mxObjectCodec constructor.
*
* References are only used for cells in mxGraph. For defining other
* referencable object types, the codec must be able to work out the ID of an
* object. This is done by implementing <mxCodec.reference>. For decoding a
* reference, the XML node with the respective id-attribute is fetched from the
* document, decoded, and stored in a lookup table for later reference. For
* looking up external objects, <mxCodec.lookup> may be implemented.
*
* Expressions:
*
* For decoding JavaScript expressions, the add-node may be used with a text
* content that contains the JavaScript expression. For example, the following
* creates a field called foo in the enclosing object and assigns it the value
* of <mxConstants.ALIGN_LEFT>.
*
* (code)
* <Object>
* <add as="foo">mxConstants.ALIGN_LEFT</add>
* </Object>
* (end)
*
* The resulting object has a field called foo with the value "left". Its XML
* representation looks as follows.
*
* (code)
* <Object foo="left"/>
* (end)
*
* This means the expression is evaluated at decoding time and the result of
* the evaluation is stored in the respective field. Valid expressions are all
* JavaScript expressions, including function definitions, which are mapped to
* functions on the resulting object.
*
* Expressions are only evaluated if <allowEval> is true.
*
* Constructor: mxObjectCodec
*
* Constructs a new codec for the specified template object.
* The variables in the optional exclude array are ignored by
* the codec. Variables in the optional idrefs array are
* turned into references in the XML. The optional mapping
* may be used to map from variable names to XML attributes.
* The argument is created as follows:
*
* (code)
* var mapping = new Object();
* mapping['variableName'] = 'attribute-name';
* (end)
*
* Parameters:
*
* template - Prototypical instance of the object to be
* encoded/decoded.
* exclude - Optional array of fieldnames to be ignored.
* idrefs - Optional array of fieldnames to be converted to/from
* references.
* mapping - Optional mapping from field- to attributenames.
*/
function mxObjectCodec(template, exclude, idrefs, mapping)
{
this.template = template;
this.exclude = (exclude != null) ? exclude : [];
this.idrefs = (idrefs != null) ? idrefs : [];
this.mapping = (mapping != null) ? mapping : [];
this.reverse = new Object();
for (var i in this.mapping)
{
this.reverse[this.mapping[i]] = i;
}
};
/**
* Variable: allowEval
*
* Static global switch that specifies if expressions in arrays are allowed.
* Default is false. NOTE: Enabling this carries a possible security risk.
*/
mxObjectCodec.allowEval = false;
/**
* Variable: template
*
* Holds the template object associated with this codec.
*/
mxObjectCodec.prototype.template = null;
/**
* Variable: exclude
*
* Array containing the variable names that should be
* ignored by the codec.
*/
mxObjectCodec.prototype.exclude = null;
/**
* Variable: idrefs
*
* Array containing the variable names that should be
* turned into or converted from references. See
* <mxCodec.getId> and <mxCodec.getObject>.
*/
mxObjectCodec.prototype.idrefs = null;
/**
* Variable: mapping
*
* Maps from from fieldnames to XML attribute names.
*/
mxObjectCodec.prototype.mapping = null;
/**
* Variable: reverse
*
* Maps from from XML attribute names to fieldnames.
*/
mxObjectCodec.prototype.reverse = null;
/**
* Function: getName
*
* Returns the name used for the nodenames and lookup of the codec when
* classes are encoded and nodes are decoded. For classes to work with
* this the codec registry automatically adds an alias for the classname
* if that is different than what this returns. The default implementation
* returns the classname of the template class.
*/
mxObjectCodec.prototype.getName = function()
{
return mxUtils.getFunctionName(this.template.constructor);
};
/**
* Function: cloneTemplate
*
* Returns a new instance of the template for this codec.
*/
mxObjectCodec.prototype.cloneTemplate = function()
{
return new this.template.constructor();
};
/**
* Function: getFieldName
*
* Returns the fieldname for the given attributename.
* Looks up the value in the <reverse> mapping or returns
* the input if there is no reverse mapping for the
* given name.
*/
mxObjectCodec.prototype.getFieldName = function(attributename)
{
if (attributename != null)
{
var mapped = this.reverse[attributename];
if (mapped != null)
{
attributename = mapped;
}
}
return attributename;
};
/**
* Function: getAttributeName
*
* Returns the attributename for the given fieldname.
* Looks up the value in the <mapping> or returns
* the input if there is no mapping for the
* given name.
*/
mxObjectCodec.prototype.getAttributeName = function(fieldname)
{
if (fieldname != null)
{
var mapped = this.mapping[fieldname];
if (mapped != null)
{
fieldname = mapped;
}
}
return fieldname;
};
/**
* Function: isExcluded
*
* Returns true if the given attribute is to be ignored by the codec. This
* implementation returns true if the given fieldname is in <exclude> or
* if the fieldname equals <mxObjectIdentity.FIELD_NAME>.
*
* Parameters:
*
* obj - Object instance that contains the field.
* attr - Fieldname of the field.
* value - Value of the field.
* write - Boolean indicating if the field is being encoded or decoded.
* Write is true if the field is being encoded, else it is being decoded.
*/
mxObjectCodec.prototype.isExcluded = function(obj, attr, value, write)
{
return attr == mxObjectIdentity.FIELD_NAME ||
mxUtils.indexOf(this.exclude, attr) >= 0;
};
/**
* Function: isReference
*
* Returns true if the given fieldname is to be treated
* as a textual reference (ID). This implementation returns
* true if the given fieldname is in <idrefs>.
*
* Parameters:
*
* obj - Object instance that contains the field.
* attr - Fieldname of the field.
* value - Value of the field.
* write - Boolean indicating if the field is being encoded or decoded.
* Write is true if the field is being encoded, else it is being decoded.
*/
mxObjectCodec.prototype.isReference = function(obj, attr, value, write)
{
return mxUtils.indexOf(this.idrefs, attr) >= 0;
};
/**
* Function: encode
*
* Encodes the specified object and returns a node
* representing then given object. Calls <beforeEncode>
* after creating the node and <afterEncode> with the
* resulting node after processing.
*
* Enc is a reference to the calling encoder. It is used
* to encode complex objects and create references.
*
* This implementation encodes all variables of an
* object according to the following rules:
*
* - If the variable name is in <exclude> then it is ignored.
* - If the variable name is in <idrefs> then <mxCodec.getId>
* is used to replace the object with its ID.
* - The variable name is mapped using <mapping>.
* - If obj is an array and the variable name is numeric
* (ie. an index) then it is not encoded.
* - If the value is an object, then the codec is used to
* create a child node with the variable name encoded into
* the "as" attribute.
* - Else, if <encodeDefaults> is true or the value differs
* from the template value, then ...
* - ... if obj is not an array, then the value is mapped to
* an attribute.
* - ... else if obj is an array, the value is mapped to an
* add child with a value attribute or a text child node,
* if the value is a function.
*
* If no ID exists for a variable in <idrefs> or if an object
* cannot be encoded, a warning is issued using <mxLog.warn>.
*
* Returns the resulting XML node that represents the given
* object.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Object to be encoded.
*/
mxObjectCodec.prototype.encode = function(enc, obj)
{
var node = enc.document.createElement(this.getName());
obj = this.beforeEncode(enc, obj, node);
this.encodeObject(enc, obj, node);
return this.afterEncode(enc, obj, node);
};
/**
* Function: encodeObject
*
* Encodes the value of each member in then given obj into the given node using
* <encodeValue>.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Object to be encoded.
* node - XML node that contains the encoded object.
*/
mxObjectCodec.prototype.encodeObject = function(enc, obj, node)
{
enc.setAttribute(node, 'id', enc.getId(obj));
for (var i in obj)
{
var name = i;
var value = obj[name];
if (value != null && !this.isExcluded(obj, name, value, true))
{
if (mxUtils.isInteger(name))
{
name = null;
}
this.encodeValue(enc, obj, name, value, node);
}
}
};
/**
* Function: encodeValue
*
* Converts the given value according to the mappings
* and id-refs in this codec and uses <writeAttribute>
* to write the attribute into the given node.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Object whose property is going to be encoded.
* name - XML node that contains the encoded object.
* value - Value of the property to be encoded.
* node - XML node that contains the encoded object.
*/
mxObjectCodec.prototype.encodeValue = function(enc, obj, name, value, node)
{
if (value != null)
{
if (this.isReference(obj, name, value, true))
{
var tmp = enc.getId(value);
if (tmp == null)
{
mxLog.warn('mxObjectCodec.encode: No ID for ' +
this.getName() + '.' + name + '=' + value);
return; // exit
}
value = tmp;
}
var defaultValue = this.template[name];
// Checks if the value is a default value and
// the name is correct
if (name == null || enc.encodeDefaults || defaultValue != value)
{
name = this.getAttributeName(name);
this.writeAttribute(enc, obj, name, value, node);
}
}
};
/**
* Function: writeAttribute
*
* Writes the given value into node using <writePrimitiveAttribute>
* or <writeComplexAttribute> depending on the type of the value.
*/
mxObjectCodec.prototype.writeAttribute = function(enc, obj, name, value, node)
{
if (typeof(value) != 'object' /* primitive type */)
{
this.writePrimitiveAttribute(enc, obj, name, value, node);
}
else /* complex type */
{
this.writeComplexAttribute(enc, obj, name, value, node);
}
};
/**
* Function: writePrimitiveAttribute
*
* Writes the given value as an attribute of the given node.
*/
mxObjectCodec.prototype.writePrimitiveAttribute = function(enc, obj, name, value, node)
{
value = this.convertAttributeToXml(enc, obj, name, value, node);
if (name == null)
{
var child = enc.document.createElement('add');
if (typeof(value) == 'function')
{
child.appendChild(enc.document.createTextNode(value));
}
else
{
enc.setAttribute(child, 'value', value);
}
node.appendChild(child);
}
else if (typeof(value) != 'function')
{
enc.setAttribute(node, name, value);
}
};
/**
* Function: writeComplexAttribute
*
* Writes the given value as a child node of the given node.
*/
mxObjectCodec.prototype.writeComplexAttribute = function(enc, obj, name, value, node)
{
var child = enc.encode(value);
if (child != null)
{
if (name != null)
{
child.setAttribute('as', name);
}
node.appendChild(child);
}
else
{
mxLog.warn('mxObjectCodec.encode: No node for ' + this.getName() + '.' + name + ': ' + value);
}
};
/**
* Function: convertAttributeToXml
*
* Converts true to "1" and false to "0" is <isBooleanAttribute> returns true.
* All other values are not converted.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Objec to convert the attribute for.
* name - Name of the attribute to be converted.
* value - Value to be converted.
*/
mxObjectCodec.prototype.convertAttributeToXml = function(enc, obj, name, value)
{
// Makes sure to encode boolean values as numeric values
if (this.isBooleanAttribute(enc, obj, name, value))
{
// Checks if the value is true (do not use the value as is, because
// this would check if the value is not null, so 0 would be true)
value = (value == true) ? '1' : '0';
}
return value;
};
/**
* Function: isBooleanAttribute
*
* Returns true if the given object attribute is a boolean value.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Objec to convert the attribute for.
* name - Name of the attribute to be converted.
* value - Value of the attribute to be converted.
*/
mxObjectCodec.prototype.isBooleanAttribute = function(enc, obj, name, value)
{
return (typeof(value.length) == 'undefined' && (value == true || value == false));
};
/**
* Function: convertAttributeFromXml
*
* Converts booleans and numeric values to the respective types. Values are
* numeric if <isNumericAttribute> returns true.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* attr - XML attribute to be converted.
* obj - Objec to convert the attribute for.
*/
mxObjectCodec.prototype.convertAttributeFromXml = function(dec, attr, obj)
{
var value = attr.value;
if (this.isNumericAttribute(dec, attr, obj))
{
value = parseFloat(value);
if (isNaN(value) || !isFinite(value))
{
value = 0;
}
}
return value;
};
/**
* Function: isNumericAttribute
*
* Returns true if the given XML attribute is or should be a numeric value.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* attr - XML attribute to be converted.
* obj - Objec to convert the attribute for.
*/
mxObjectCodec.prototype.isNumericAttribute = function(dec, attr, obj)
{
// Handles known numeric attributes for generic objects
var result = (obj.constructor == mxGeometry &&
(attr.name == 'x' || attr.name == 'y' ||
attr.name == 'width' || attr.name == 'height')) ||
(obj.constructor == mxPoint &&
(attr.name == 'x' || attr.name == 'y')) ||
mxUtils.isNumeric(attr.value);
return result;
};
/**
* Function: beforeEncode
*
* Hook for subclassers to pre-process the object before
* encoding. This returns the input object. The return
* value of this function is used in <encode> to perform
* the default encoding into the given node.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Object to be encoded.
* node - XML node to encode the object into.
*/
mxObjectCodec.prototype.beforeEncode = function(enc, obj, node)
{
return obj;
};
/**
* Function: afterEncode
*
* Hook for subclassers to post-process the node
* for the given object after encoding and return the
* post-processed node. This implementation returns
* the input node. The return value of this method
* is returned to the encoder from <encode>.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* obj - Object to be encoded.
* node - XML node that represents the default encoding.
*/
mxObjectCodec.prototype.afterEncode = function(enc, obj, node)
{
return node;
};
/**
* Function: decode
*
* Parses the given node into the object or returns a new object
* representing the given node.
*
* Dec is a reference to the calling decoder. It is used to decode
* complex objects and resolve references.
*
* If a node has an id attribute then the object cache is checked for the
* object. If the object is not yet in the cache then it is constructed
* using the constructor of <template> and cached in <mxCodec.objects>.
*
* This implementation decodes all attributes and childs of a node
* according to the following rules:
*
* - If the variable name is in <exclude> or if the attribute name is "id"
* or "as" then it is ignored.
* - If the variable name is in <idrefs> then <mxCodec.getObject> is used
* to replace the reference with an object.
* - The variable name is mapped using a reverse <mapping>.
* - If the value has a child node, then the codec is used to create a
* child object with the variable name taken from the "as" attribute.
* - If the object is an array and the variable name is empty then the
* value or child object is appended to the array.
* - If an add child has no value or the object is not an array then
* the child text content is evaluated using <mxUtils.eval>.
*
* For add nodes where the object is not an array and the variable name
* is defined, the default mechanism is used, allowing to override/add
* methods as follows:
*
* (code)
* <Object>
* <add as="hello"><![CDATA[
* function(arg1) {
* mxUtils.alert('Hello '+arg1);
* }
* ]]></add>
* </Object>
* (end)
*
* If no object exists for an ID in <idrefs> a warning is issued
* using <mxLog.warn>.
*
* Returns the resulting object that represents the given XML node
* or the object given to the method as the into parameter.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* node - XML node to be decoded.
* into - Optional objec to encode the node into.
*/
mxObjectCodec.prototype.decode = function(dec, node, into)
{
var id = node.getAttribute('id');
var obj = dec.objects[id];
if (obj == null)
{
obj = into || this.cloneTemplate();
if (id != null)
{
dec.putObject(id, obj);
}
}
node = this.beforeDecode(dec, node, obj);
this.decodeNode(dec, node, obj);
return this.afterDecode(dec, node, obj);
};
/**
* Function: decodeNode
*
* Calls <decodeAttributes> and <decodeChildren> for the given node.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* node - XML node to be decoded.
* obj - Objec to encode the node into.
*/
mxObjectCodec.prototype.decodeNode = function(dec, node, obj)
{
if (node != null)
{
this.decodeAttributes(dec, node, obj);
this.decodeChildren(dec, node, obj);
}
};
/**
* Function: decodeAttributes
*
* Decodes all attributes of the given node using <decodeAttribute>.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* node - XML node to be decoded.
* obj - Objec to encode the node into.
*/
mxObjectCodec.prototype.decodeAttributes = function(dec, node, obj)
{
var attrs = node.attributes;
if (attrs != null)
{
for (var i = 0; i < attrs.length; i++)
{
this.decodeAttribute(dec, attrs[i], obj);
}
}
};
/**
* Function: isIgnoredAttribute
*
* Returns true if the given attribute should be ignored. This implementation
* returns true if the attribute name is "as" or "id".
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* attr - XML attribute to be decoded.
* obj - Objec to encode the attribute into.
*/
mxObjectCodec.prototype.isIgnoredAttribute = function(dec, attr, obj)
{
return attr.nodeName == 'as' || attr.nodeName == 'id';
};
/**
* Function: decodeAttribute
*
* Reads the given attribute into the specified object.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* attr - XML attribute to be decoded.
* obj - Objec to encode the attribute into.
*/
mxObjectCodec.prototype.decodeAttribute = function(dec, attr, obj)
{
if (!this.isIgnoredAttribute(dec, attr, obj))
{
var name = attr.nodeName;
// Converts the string true and false to their boolean values.
// This may require an additional check on the obj to see if
// the existing field is a boolean value or uninitialized, in
// which case we may want to convert true and false to a string.
var value = this.convertAttributeFromXml(dec, attr, obj);
var fieldname = this.getFieldName(name);
if (this.isReference(obj, fieldname, value, false))
{
var tmp = dec.getObject(value);
if (tmp == null)
{
mxLog.warn('mxObjectCodec.decode: No object for ' +
this.getName() + '.' + name + '=' + value);
return; // exit
}
value = tmp;
}
if (!this.isExcluded(obj, name, value, false))
{
//mxLog.debug(mxUtils.getFunctionName(obj.constructor)+'.'+name+'='+value);
obj[name] = value;
}
}
};
/**
* Function: decodeChildren
*
* Decodes all children of the given node using <decodeChild>.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* node - XML node to be decoded.
* obj - Objec to encode the node into.
*/
mxObjectCodec.prototype.decodeChildren = function(dec, node, obj)
{
var child = node.firstChild;
while (child != null)
{
var tmp = child.nextSibling;
if (child.nodeType == mxConstants.NODETYPE_ELEMENT &&
!this.processInclude(dec, child, obj))
{
this.decodeChild(dec, child, obj);
}
child = tmp;
}
};
/**
* Function: decodeChild
*
* Reads the specified child into the given object.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* child - XML child element to be decoded.
* obj - Objec to encode the node into.
*/
mxObjectCodec.prototype.decodeChild = function(dec, child, obj)
{
var fieldname = this.getFieldName(child.getAttribute('as'));
if (fieldname == null || !this.isExcluded(obj, fieldname, child, false))
{
var template = this.getFieldTemplate(obj, fieldname, child);
var value = null;
if (child.nodeName == 'add')
{
value = child.getAttribute('value');
if (value == null && mxObjectCodec.allowEval)
{
value = mxUtils.eval(mxUtils.getTextContent(child));
}
}
else
{
value = dec.decode(child, template);
}
try
{
this.addObjectValue(obj, fieldname, value, template);
}
catch (e)
{
throw new Error(e.message + ' for ' + child.nodeName);
}
}
};
/**
* Function: getFieldTemplate
*
* Returns the template instance for the given field. This returns the
* value of the field, null if the value is an array or an empty collection
* if the value is a collection. The value is then used to populate the
* field for a new instance. For strongly typed languages it may be
* required to override this to return the correct collection instance
* based on the encoded child.
*/
mxObjectCodec.prototype.getFieldTemplate = function(obj, fieldname, child)
{
var template = obj[fieldname];
// Non-empty arrays are replaced completely
if (template instanceof Array && template.length > 0)
{
template = null;
}
return template;
};
/**
* Function: addObjectValue
*
* Sets the decoded child node as a value of the given object. If the
* object is a map, then the value is added with the given fieldname as a
* key. If the fieldname is not empty, then setFieldValue is called or
* else, if the object is a collection, the value is added to the
* collection. For strongly typed languages it may be required to
* override this with the correct code to add an entry to an object.
*/
mxObjectCodec.prototype.addObjectValue = function(obj, fieldname, value, template)
{
if (value != null && value != template)
{
if (fieldname != null && fieldname.length > 0)
{
obj[fieldname] = value;
}
else
{
obj.push(value);
}
//mxLog.debug('Decoded '+mxUtils.getFunctionName(obj.constructor)+'.'+fieldname+': '+value);
}
};
/**
* Function: processInclude
*
* Returns true if the given node is an include directive and
* executes the include by decoding the XML document. Returns
* false if the given node is not an include directive.
*
* Parameters:
*
* dec - <mxCodec> that controls the encoding/decoding process.
* node - XML node to be checked.
* into - Optional object to pass-thru to the codec.
*/
mxObjectCodec.prototype.processInclude = function(dec, node, into)
{
if (node.nodeName == 'include')
{
var name = node.getAttribute('name');
if (name != null)
{
try
{
var xml = mxUtils.load(name).getDocumentElement();
if (xml != null)
{
dec.decode(xml, into);
}
}
catch (e)
{
// ignore
}
}
return true;
}
return false;
};
/**
* Function: beforeDecode
*
* Hook for subclassers to pre-process the node for
* the specified object and return the node to be
* used for further processing by <decode>.
* The object is created based on the template in the
* calling method and is never null. This implementation
* returns the input node. The return value of this
* function is used in <decode> to perform
* the default decoding into the given object.
*
* Parameters:
*
* dec - <mxCodec> that controls the decoding process.
* node - XML node to be decoded.
* obj - Object to encode the node into.
*/
mxObjectCodec.prototype.beforeDecode = function(dec, node, obj)
{
return node;
};
/**
* Function: afterDecode
*
* Hook for subclassers to post-process the object after
* decoding. This implementation returns the given object
* without any changes. The return value of this method
* is returned to the decoder from <decode>.
*
* Parameters:
*
* enc - <mxCodec> that controls the encoding process.
* node - XML node to be decoded.
* obj - Object that represents the default decoding.
*/
mxObjectCodec.prototype.afterDecode = function(dec, node, obj)
{
return obj;
};
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxCellCodec
*
* Codec for <mxCell>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - children
* - edges
* - overlays
* - mxTransient
*
* Reference Fields:
*
* - parent
* - source
* - target
*
* Transient fields can be added using the following code:
*
* mxCodecRegistry.getCodec(mxCell).exclude.push('name_of_field');
*
* To subclass <mxCell>, replace the template and add an alias as
* follows.
*
* (code)
* function CustomCell(value, geometry, style)
* {
* mxCell.apply(this, arguments);
* }
*
* mxUtils.extend(CustomCell, mxCell);
*
* mxCodecRegistry.getCodec(mxCell).template = new CustomCell();
* mxCodecRegistry.addAlias('CustomCell', 'mxCell');
* (end)
*/
var codec = new mxObjectCodec(new mxCell(),
['children', 'edges', 'overlays', 'mxTransient'],
['parent', 'source', 'target']);
/**
* Function: isCellCodec
*
* Returns true since this is a cell codec.
*/
codec.isCellCodec = function()
{
return true;
};
/**
* Overidden to disable conversion of value to number.
*/
codec.isNumericAttribute = function(dec, attr, obj)
{
return attr.nodeName !== 'value' && mxObjectCodec.prototype.isNumericAttribute.apply(this, arguments);
};
/**
* Function: isExcluded
*
* Excludes user objects that are XML nodes.
*/
codec.isExcluded = function(obj, attr, value, isWrite)
{
return mxObjectCodec.prototype.isExcluded.apply(this, arguments) ||
(isWrite && attr == 'value' &&
value.nodeType == mxConstants.NODETYPE_ELEMENT);
};
/**
* Function: afterEncode
*
* Encodes an <mxCell> and wraps the XML up inside the
* XML of the user object (inversion).
*/
codec.afterEncode = function(enc, obj, node)
{
if (obj.value != null && obj.value.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Wraps the graphical annotation up in the user object (inversion)
// by putting the result of the default encoding into a clone of the
// user object (node type 1) and returning this cloned user object.
var tmp = node;
node = mxUtils.importNode(enc.document, obj.value, true);
node.appendChild(tmp);
// Moves the id attribute to the outermost XML node, namely the
// node which denotes the object boundaries in the file.
var id = tmp.getAttribute('id');
node.setAttribute('id', id);
tmp.removeAttribute('id');
}
return node;
};
/**
* Function: beforeDecode
*
* Decodes an <mxCell> and uses the enclosing XML node as
* the user object for the cell (inversion).
*/
codec.beforeDecode = function(dec, node, obj)
{
var inner = node.cloneNode(true);
var classname = this.getName();
if (node.nodeName != classname)
{
// Passes the inner graphical annotation node to the
// object codec for further processing of the cell.
var tmp = node.getElementsByTagName(classname)[0];
if (tmp != null && tmp.parentNode == node)
{
mxUtils.removeWhitespace(tmp, true);
mxUtils.removeWhitespace(tmp, false);
tmp.parentNode.removeChild(tmp);
inner = tmp;
}
else
{
inner = null;
}
// Creates the user object out of the XML node
obj.value = node.cloneNode(true);
var id = obj.value.getAttribute('id');
if (id != null)
{
obj.setId(id);
obj.value.removeAttribute('id');
}
}
else
{
// Uses ID from XML file as ID for cell in model
obj.setId(node.getAttribute('id'));
}
// Preprocesses and removes all Id-references in order to use the
// correct encoder (this) for the known references to cells (all).
if (inner != null)
{
for (var i = 0; i < this.idrefs.length; i++)
{
var attr = this.idrefs[i];
var ref = inner.getAttribute(attr);
if (ref != null)
{
inner.removeAttribute(attr);
var object = dec.objects[ref] || dec.lookup(ref);
if (object == null)
{
// Needs to decode forward reference
var element = dec.getElementById(ref);
if (element != null)
{
var decoder = mxCodecRegistry.codecs[element.nodeName] || this;
object = decoder.decode(dec, element);
}
}
obj[attr] = object;
}
}
}
return inner;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxModelCodec
*
* Codec for <mxGraphModel>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec>
* and the <mxCodecRegistry>.
*/
var codec = new mxObjectCodec(new mxGraphModel());
/**
* Function: encodeObject
*
* Encodes the given <mxGraphModel> by writing a (flat) XML sequence of
* cell nodes as produced by the <mxCellCodec>. The sequence is
* wrapped-up in a node with the name root.
*/
codec.encodeObject = function(enc, obj, node)
{
var rootNode = enc.document.createElement('root');
enc.encodeCell(obj.getRoot(), rootNode);
node.appendChild(rootNode);
};
/**
* Function: decodeChild
*
* Overrides decode child to handle special child nodes.
*/
codec.decodeChild = function(dec, child, obj)
{
if (child.nodeName == 'root')
{
this.decodeRoot(dec, child, obj);
}
else
{
mxObjectCodec.prototype.decodeChild.apply(this, arguments);
}
};
/**
* Function: decodeRoot
*
* Reads the cells into the graph model. All cells
* are children of the root element in the node.
*/
codec.decodeRoot = function(dec, root, model)
{
var rootCell = null;
var tmp = root.firstChild;
while (tmp != null)
{
var cell = dec.decodeCell(tmp);
if (cell != null && cell.getParent() == null)
{
rootCell = cell;
}
tmp = tmp.nextSibling;
}
// Sets the root on the model if one has been decoded
if (rootCell != null)
{
model.setRoot(rootCell);
}
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxRootChangeCodec
*
* Codec for <mxRootChange>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
* - root
*/
var codec = new mxObjectCodec(new mxRootChange(),
['model', 'previous', 'root']);
/**
* Function: onEncode
*
* Encodes the child recursively.
*/
codec.afterEncode = function(enc, obj, node)
{
enc.encodeCell(obj.root, node);
return node;
};
/**
* Function: beforeDecode
*
* Decodes the optional children as cells
* using the respective decoder.
*/
codec.beforeDecode = function(dec, node, obj)
{
if (node.firstChild != null &&
node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Makes sure the original node isn't modified
node = node.cloneNode(true);
var tmp = node.firstChild;
obj.root = dec.decodeCell(tmp, false);
var tmp2 = tmp.nextSibling;
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
while (tmp != null)
{
tmp2 = tmp.nextSibling;
dec.decodeCell(tmp);
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
}
}
return node;
};
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
obj.previous = obj.root;
return obj;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxChildChangeCodec
*
* Codec for <mxChildChange>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
* - previousIndex
* - child
*
* Reference Fields:
*
* - parent
*/
var codec = new mxObjectCodec(new mxChildChange(),
['model', 'child', 'previousIndex'],
['parent', 'previous']);
/**
* Function: isReference
*
* Returns true for the child attribute if the child
* cell had a previous parent or if we're reading the
* child as an attribute rather than a child node, in
* which case it's always a reference.
*/
codec.isReference = function(obj, attr, value, isWrite)
{
if (attr == 'child' && (!isWrite || obj.model.contains(obj.previous)))
{
return true;
}
return mxUtils.indexOf(this.idrefs, attr) >= 0;
};
/**
* Function: isExcluded
*
* Excludes references to parent or previous if not in the model.
*/
codec.isExcluded = function(obj, attr, value, write)
{
return mxObjectCodec.prototype.isExcluded.apply(this, arguments) ||
(write && value != null && (attr == 'previous' ||
attr == 'parent') && !obj.model.contains(value));
};
/**
* Function: afterEncode
*
* Encodes the child recusively and adds the result
* to the given node.
*/
codec.afterEncode = function(enc, obj, node)
{
if (this.isReference(obj, 'child', obj.child, true))
{
// Encodes as reference (id)
node.setAttribute('child', enc.getId(obj.child));
}
else
{
// At this point, the encoder is no longer able to know which cells
// are new, so we have to encode the complete cell hierarchy and
// ignore the ones that are already there at decoding time. Note:
// This can only be resolved by moving the notify event into the
// execute of the edit.
enc.encodeCell(obj.child, node);
}
return node;
};
/**
* Function: beforeDecode
*
* Decodes the any child nodes as using the respective
* codec from the registry.
*/
codec.beforeDecode = function(dec, node, obj)
{
if (node.firstChild != null &&
node.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Makes sure the original node isn't modified
node = node.cloneNode(true);
var tmp = node.firstChild;
obj.child = dec.decodeCell(tmp, false);
var tmp2 = tmp.nextSibling;
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
while (tmp != null)
{
tmp2 = tmp.nextSibling;
if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT)
{
// Ignores all existing cells because those do not need to
// be re-inserted into the model. Since the encoded version
// of these cells contains the new parent, this would leave
// to an inconsistent state on the model (ie. a parent
// change without a call to parentForCellChanged).
var id = tmp.getAttribute('id');
if (dec.lookup(id) == null)
{
dec.decodeCell(tmp);
}
}
tmp.parentNode.removeChild(tmp);
tmp = tmp2;
}
}
else
{
var childRef = node.getAttribute('child');
obj.child = dec.getObject(childRef);
}
return node;
};
/**
* Function: afterDecode
*
* Restores object state in the child change.
*/
codec.afterDecode = function(dec, node, obj)
{
// Cells are decoded here after a complete transaction so the previous
// parent must be restored on the cell for the case where the cell was
// added. This is needed for the local model to identify the cell as a
// new cell and register the ID.
if (obj.child != null)
{
if (obj.child.parent != null && obj.previous != null &&
obj.child.parent != obj.previous)
{
obj.previous = obj.child.parent;
}
obj.child.parent = obj.previous;
obj.previous = obj.parent;
obj.previousIndex = obj.index;
}
return obj;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxTerminalChangeCodec
*
* Codec for <mxTerminalChange>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec> and
* the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
*
* Reference Fields:
*
* - cell
* - terminal
*/
var codec = new mxObjectCodec(new mxTerminalChange(),
['model', 'previous'], ['cell', 'terminal']);
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
obj.previous = obj.terminal;
return obj;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxGenericChangeCodec
*
* Codec for <mxValueChange>s, <mxStyleChange>s, <mxGeometryChange>s,
* <mxCollapseChange>s and <mxVisibleChange>s. This class is created
* and registered dynamically at load time and used implicitely
* via <mxCodec> and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - model
* - previous
*
* Reference Fields:
*
* - cell
*
* Constructor: mxGenericChangeCodec
*
* Factory function that creates a <mxObjectCodec> for
* the specified change and fieldname.
*
* Parameters:
*
* obj - An instance of the change object.
* variable - The fieldname for the change data.
*/
var mxGenericChangeCodec = function(obj, variable)
{
var codec = new mxObjectCodec(obj, ['model', 'previous'], ['cell']);
/**
* Function: afterDecode
*
* Restores the state by assigning the previous value.
*/
codec.afterDecode = function(dec, node, obj)
{
// Allows forward references in sessions. This is a workaround
// for the sequence of edits in mxGraph.moveCells and cellsAdded.
if (mxUtils.isNode(obj.cell))
{
obj.cell = dec.decodeCell(obj.cell, false);
}
obj.previous = obj[variable];
return obj;
};
return codec;
};
// Registers the codecs
mxCodecRegistry.register(mxGenericChangeCodec(new mxValueChange(), 'value'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxStyleChange(), 'style'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxGeometryChange(), 'geometry'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxCollapseChange(), 'collapsed'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxVisibleChange(), 'visible'));
mxCodecRegistry.register(mxGenericChangeCodec(new mxCellAttributeChange(), 'value'));
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxGraphCodec
*
* Codec for <mxGraph>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - graphListeners
* - eventListeners
* - view
* - container
* - cellRenderer
* - editor
* - selection
*/
return new mxObjectCodec(new mxGraph(),
['graphListeners', 'eventListeners', 'view', 'container',
'cellRenderer', 'editor', 'selection']);
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxGraphViewCodec
*
* Custom encoder for <mxGraphView>s. This class is created
* and registered dynamically at load time and used implicitely via
* <mxCodec> and the <mxCodecRegistry>. This codec only writes views
* into a XML format that can be used to create an image for
* the graph, that is, it contains absolute coordinates with
* computed perimeters, edge styles and cell styles.
*/
var codec = new mxObjectCodec(new mxGraphView());
/**
* Function: encode
*
* Encodes the given <mxGraphView> using <encodeCell>
* starting at the model's root. This returns the
* top-level graph node of the recursive encoding.
*/
codec.encode = function(enc, view)
{
return this.encodeCell(enc, view,
view.graph.getModel().getRoot());
};
/**
* Function: encodeCell
*
* Recursively encodes the specifed cell. Uses layer
* as the default nodename. If the cell's parent is
* null, then graph is used for the nodename. If
* <mxGraphModel.isEdge> returns true for the cell,
* then edge is used for the nodename, else if
* <mxGraphModel.isVertex> returns true for the cell,
* then vertex is used for the nodename.
*
* <mxGraph.getLabel> is used to create the label
* attribute for the cell. For graph nodes and vertices
* the bounds are encoded into x, y, width and height.
* For edges the points are encoded into a points
* attribute as a space-separated list of comma-separated
* coordinate pairs (eg. x0,y0 x1,y1 ... xn,yn). All
* values from the cell style are added as attribute
* values to the node.
*/
codec.encodeCell = function(enc, view, cell)
{
var model = view.graph.getModel();
var state = view.getState(cell);
var parent = model.getParent(cell);
if (parent == null || state != null)
{
var childCount = model.getChildCount(cell);
var geo = view.graph.getCellGeometry(cell);
var name = null;
if (parent == model.getRoot())
{
name = 'layer';
}
else if (parent == null)
{
name = 'graph';
}
else if (model.isEdge(cell))
{
name = 'edge';
}
else if (childCount > 0 && geo != null)
{
name = 'group';
}
else if (model.isVertex(cell))
{
name = 'vertex';
}
if (name != null)
{
var node = enc.document.createElement(name);
var lab = view.graph.getLabel(cell);
if (lab != null)
{
node.setAttribute('label', view.graph.getLabel(cell));
if (view.graph.isHtmlLabel(cell))
{
node.setAttribute('html', true);
}
}
if (parent == null)
{
var bounds = view.getGraphBounds();
if (bounds != null)
{
node.setAttribute('x', Math.round(bounds.x));
node.setAttribute('y', Math.round(bounds.y));
node.setAttribute('width', Math.round(bounds.width));
node.setAttribute('height', Math.round(bounds.height));
}
node.setAttribute('scale', view.scale);
}
else if (state != null && geo != null)
{
// Writes each key, value in the style pair to an attribute
for (var i in state.style)
{
var value = state.style[i];
// Tries to turn objects and functions into strings
if (typeof(value) == 'function' &&
typeof(value) == 'object')
{
value = mxStyleRegistry.getName(value);
}
if (value != null &&
typeof(value) != 'function' &&
typeof(value) != 'object')
{
node.setAttribute(i, value);
}
}
var abs = state.absolutePoints;
// Writes the list of points into one attribute
if (abs != null && abs.length > 0)
{
var pts = Math.round(abs[0].x) + ',' + Math.round(abs[0].y);
for (var i=1; i<abs.length; i++)
{
pts += ' ' + Math.round(abs[i].x) + ',' +
Math.round(abs[i].y);
}
node.setAttribute('points', pts);
}
// Writes the bounds into 4 attributes
else
{
node.setAttribute('x', Math.round(state.x));
node.setAttribute('y', Math.round(state.y));
node.setAttribute('width', Math.round(state.width));
node.setAttribute('height', Math.round(state.height));
}
var offset = state.absoluteOffset;
// Writes the offset into 2 attributes
if (offset != null)
{
if (offset.x != 0)
{
node.setAttribute('dx', Math.round(offset.x));
}
if (offset.y != 0)
{
node.setAttribute('dy', Math.round(offset.y));
}
}
}
for (var i=0; i<childCount; i++)
{
var childNode = this.encodeCell(enc,
view, model.getChildAt(cell, i));
if (childNode != null)
{
node.appendChild(childNode);
}
}
}
}
return node;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStylesheetCodec
*
* Codec for <mxStylesheet>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec>
* and the <mxCodecRegistry>.
*/
var mxStylesheetCodec = mxCodecRegistry.register(function()
{
var codec = new mxObjectCodec(new mxStylesheet());
/**
* Function: encode
*
* Encodes a stylesheet. See <decode> for a description of the
* format.
*/
codec.encode = function(enc, obj)
{
var node = enc.document.createElement(this.getName());
for (var i in obj.styles)
{
var style = obj.styles[i];
var styleNode = enc.document.createElement('add');
if (i != null)
{
styleNode.setAttribute('as', i);
for (var j in style)
{
var value = this.getStringValue(j, style[j]);
if (value != null)
{
var entry = enc.document.createElement('add');
entry.setAttribute('value', value);
entry.setAttribute('as', j);
styleNode.appendChild(entry);
}
}
if (styleNode.childNodes.length > 0)
{
node.appendChild(styleNode);
}
}
}
return node;
};
/**
* Function: getStringValue
*
* Returns the string for encoding the given value.
*/
codec.getStringValue = function(key, value)
{
var type = typeof(value);
if (type == 'function')
{
value = mxStyleRegistry.getName(value);
}
else if (type == 'object')
{
value = null;
}
return value;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Adds a new style.
*
* Attributes:
*
* as - Name of the style.
* extend - Name of the style to inherit from.
*
* Each node contains another sequence of add and remove nodes with the following
* attributes:
*
* as - Name of the style (see <mxConstants>).
* value - Value for the style.
*
* Instead of the value-attribute, one can put Javascript expressions into
* the node as follows if <mxStylesheetCodec.allowEval> is true:
* <add as="perimeter">mxPerimeter.RectanglePerimeter</add>
*
* A remove node will remove the entry with the name given in the as-attribute
* from the style.
*
* Example:
*
* (code)
* <mxStylesheet as="stylesheet">
* <add as="text">
* <add as="fontSize" value="12"/>
* </add>
* <add as="defaultVertex" extend="text">
* <add as="shape" value="rectangle"/>
* </add>
* </mxStylesheet>
* (end)
*/
codec.decode = function(dec, node, into)
{
var obj = into || new this.template.constructor();
var id = node.getAttribute('id');
if (id != null)
{
dec.objects[id] = obj;
}
node = node.firstChild;
while (node != null)
{
if (!this.processInclude(dec, node, obj) && node.nodeName == 'add')
{
var as = node.getAttribute('as');
if (as != null)
{
var extend = node.getAttribute('extend');
var style = (extend != null) ? mxUtils.clone(obj.styles[extend]) : null;
if (style == null)
{
if (extend != null)
{
mxLog.warn('mxStylesheetCodec.decode: stylesheet ' +
extend + ' not found to extend');
}
style = new Object();
}
var entry = node.firstChild;
while (entry != null)
{
if (entry.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var key = entry.getAttribute('as');
if (entry.nodeName == 'add')
{
var text = mxUtils.getTextContent(entry);
var value = null;
if (text != null && text.length > 0 && mxStylesheetCodec.allowEval)
{
value = mxUtils.eval(text);
}
else
{
value = entry.getAttribute('value');
if (mxUtils.isNumeric(value))
{
value = parseFloat(value);
}
}
if (value != null)
{
style[key] = value;
}
}
else if (entry.nodeName == 'remove')
{
delete style[key];
}
}
entry = entry.nextSibling;
}
obj.putCellStyle(as, style);
}
}
node = node.nextSibling;
}
return obj;
};
// Returns the codec into the registry
return codec;
}());
/**
* Variable: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content. Default is true. Set this to false if stylesheets
* may contain user input.
*/
mxStylesheetCodec.allowEval = true;
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxDefaultKeyHandlerCodec
*
* Custom codec for configuring <mxDefaultKeyHandler>s. This class is created
* and registered dynamically at load time and used implicitely via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing key handlers, it does not encode or create key handlers.
*/
var codec = new mxObjectCodec(new mxDefaultKeyHandler());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Binds a keystroke to an actionname.
*
* Attributes:
*
* as - Keycode.
* action - Actionname to execute in editor.
* control - Optional boolean indicating if
* the control key must be pressed.
*
* Example:
*
* (code)
* <mxDefaultKeyHandler as="keyHandler">
* <add as="88" control="true" action="cut"/>
* <add as="67" control="true" action="copy"/>
* <add as="86" control="true" action="paste"/>
* </mxDefaultKeyHandler>
* (end)
*
* The keycodes are for the x, c and v keys.
*
* See also: <mxDefaultKeyHandler.bindAction>,
* http://www.js-examples.com/page/tutorials__key_codes.html
*/
codec.decode = function(dec, node, into)
{
if (into != null)
{
var editor = into.editor;
node = node.firstChild;
while (node != null)
{
if (!this.processInclude(dec, node, into) &&
node.nodeName == 'add')
{
var as = node.getAttribute('as');
var action = node.getAttribute('action');
var control = node.getAttribute('control');
into.bindAction(as, action, control);
}
node = node.nextSibling;
}
}
return into;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxDefaultToolbarCodec
*
* Custom codec for configuring <mxDefaultToolbar>s. This class is created
* and registered dynamically at load time and used implicitely via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing toolbars handlers, it does not encode or create toolbars.
*/
var mxDefaultToolbarCodec = mxCodecRegistry.register(function()
{
var codec = new mxObjectCodec(new mxDefaultToolbar());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Reads a sequence of the following child nodes
* and attributes:
*
* Child Nodes:
*
* add - Adds a new item to the toolbar. See below for attributes.
* separator - Adds a vertical separator. No attributes.
* hr - Adds a horizontal separator. No attributes.
* br - Adds a linefeed. No attributes.
*
* Attributes:
*
* as - Resource key for the label.
* action - Name of the action to execute in enclosing editor.
* mode - Modename (see below).
* template - Template name for cell insertion.
* style - Optional style to override the template style.
* icon - Icon (relative/absolute URL).
* pressedIcon - Optional icon for pressed state (relative/absolute URL).
* id - Optional ID to be used for the created DOM element.
* toggle - Optional 0 or 1 to disable toggling of the element. Default is
* 1 (true).
*
* The action, mode and template attributes are mutually exclusive. The
* style can only be used with the template attribute. The add node may
* contain another sequence of add nodes with as and action attributes
* to create a combo box in the toolbar. If the icon is specified then
* a list of the child node is expected to have its template attribute
* set and the action is ignored instead.
*
* Nodes with a specified template may define a function to be used for
* inserting the cloned template into the graph. Here is an example of such
* a node:
*
* (code)
* <add as="Swimlane" template="swimlane" icon="images/swimlane.gif"><![CDATA[
* function (editor, cell, evt, targetCell)
* {
* var pt = mxUtils.convertPoint(
* editor.graph.container, mxEvent.getClientX(evt),
* mxEvent.getClientY(evt));
* return editor.addVertex(targetCell, cell, pt.x, pt.y);
* }
* ]]></add>
* (end)
*
* In the above function, editor is the enclosing <mxEditor> instance, cell
* is the clone of the template, evt is the mouse event that represents the
* drop and targetCell is the cell under the mousepointer where the drop
* occurred. The targetCell is retrieved using <mxGraph.getCellAt>.
*
* Futhermore, nodes with the mode attribute may define a function to
* be executed upon selection of the respective toolbar icon. In the
* example below, the default edge style is set when this specific
* connect-mode is activated:
*
* (code)
* <add as="connect" mode="connect"><![CDATA[
* function (editor)
* {
* if (editor.defaultEdge != null)
* {
* editor.defaultEdge.style = 'straightEdge';
* }
* }
* ]]></add>
* (end)
*
* Both functions require <mxDefaultToolbarCodec.allowEval> to be set to true.
*
* Modes:
*
* select - Left mouse button used for rubberband- & cell-selection.
* connect - Allows connecting vertices by inserting new edges.
* pan - Disables selection and switches to panning on the left button.
*
* Example:
*
* To add items to the toolbar:
*
* (code)
* <mxDefaultToolbar as="toolbar">
* <add as="save" action="save" icon="images/save.gif"/>
* <br/><hr/>
* <add as="select" mode="select" icon="images/select.gif"/>
* <add as="connect" mode="connect" icon="images/connect.gif"/>
* </mxDefaultToolbar>
* (end)
*/
codec.decode = function(dec, node, into)
{
if (into != null)
{
var editor = into.editor;
node = node.firstChild;
while (node != null)
{
if (node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
if (!this.processInclude(dec, node, into))
{
if (node.nodeName == 'separator')
{
into.addSeparator();
}
else if (node.nodeName == 'br')
{
into.toolbar.addBreak();
}
else if (node.nodeName == 'hr')
{
into.toolbar.addLine();
}
else if (node.nodeName == 'add')
{
var as = node.getAttribute('as');
as = mxResources.get(as) || as;
var icon = node.getAttribute('icon');
var pressedIcon = node.getAttribute('pressedIcon');
var action = node.getAttribute('action');
var mode = node.getAttribute('mode');
var template = node.getAttribute('template');
var toggle = node.getAttribute('toggle') != '0';
var text = mxUtils.getTextContent(node);
var elt = null;
if (action != null)
{
elt = into.addItem(as, icon, action, pressedIcon);
}
else if (mode != null)
{
var funct = (mxDefaultToolbarCodec.allowEval) ? mxUtils.eval(text) : null;
elt = into.addMode(as, icon, mode, pressedIcon, funct);
}
else if (template != null || (text != null && text.length > 0))
{
var cell = editor.templates[template];
var style = node.getAttribute('style');
if (cell != null && style != null)
{
cell = editor.graph.cloneCell(cell);
cell.setStyle(style);
}
var insertFunction = null;
if (text != null && text.length > 0 && mxDefaultToolbarCodec.allowEval)
{
insertFunction = mxUtils.eval(text);
}
elt = into.addPrototype(as, icon, cell, pressedIcon, insertFunction, toggle);
}
else
{
var children = mxUtils.getChildNodes(node);
if (children.length > 0)
{
if (icon == null)
{
var combo = into.addActionCombo(as);
for (var i=0; i<children.length; i++)
{
var child = children[i];
if (child.nodeName == 'separator')
{
into.addOption(combo, '---');
}
else if (child.nodeName == 'add')
{
var lab = child.getAttribute('as');
var act = child.getAttribute('action');
into.addActionOption(combo, lab, act);
}
}
}
else
{
var select = null;
var create = function()
{
var template = editor.templates[select.value];
if (template != null)
{
var clone = template.clone();
var style = select.options[select.selectedIndex].cellStyle;
if (style != null)
{
clone.setStyle(style);
}
return clone;
}
else
{
mxLog.warn('Template '+template+' not found');
}
return null;
};
var img = into.addPrototype(as, icon, create, null, null, toggle);
select = into.addCombo();
// Selects the toolbar icon if a selection change
// is made in the corresponding combobox.
mxEvent.addListener(select, 'change', function()
{
into.toolbar.selectMode(img, function(evt)
{
var pt = mxUtils.convertPoint(editor.graph.container,
mxEvent.getClientX(evt), mxEvent.getClientY(evt));
return editor.addVertex(null, funct(), pt.x, pt.y);
});
into.toolbar.noReset = false;
});
// Adds the entries to the combobox
for (var i=0; i<children.length; i++)
{
var child = children[i];
if (child.nodeName == 'separator')
{
into.addOption(select, '---');
}
else if (child.nodeName == 'add')
{
var lab = child.getAttribute('as');
var tmp = child.getAttribute('template');
var option = into.addOption(select, lab, tmp || template);
option.cellStyle = child.getAttribute('style');
}
}
}
}
}
// Assigns an ID to the created element to access it later.
if (elt != null)
{
var id = node.getAttribute('id');
if (id != null && id.length > 0)
{
elt.setAttribute('id', id);
}
}
}
}
}
node = node.nextSibling;
}
}
return into;
};
// Returns the codec into the registry
return codec;
}());
/**
* Variable: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content. Default is true. Set this to false if stylesheets
* may contain user input
*/
mxDefaultToolbarCodec.allowEval = true;
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxDefaultPopupMenuCodec
*
* Custom codec for configuring <mxDefaultPopupMenu>s. This class is created
* and registered dynamically at load time and used implicitely via
* <mxCodec> and the <mxCodecRegistry>. This codec only reads configuration
* data for existing popup menus, it does not encode or create menus. Note
* that this codec only passes the configuration node to the popup menu,
* which uses the config to dynamically create menus. See
* <mxDefaultPopupMenu.createMenu>.
*/
var codec = new mxObjectCodec(new mxDefaultPopupMenu());
/**
* Function: encode
*
* Returns null.
*/
codec.encode = function(enc, obj)
{
return null;
};
/**
* Function: decode
*
* Uses the given node as the config for <mxDefaultPopupMenu>.
*/
codec.decode = function(dec, node, into)
{
var inc = node.getElementsByTagName('include')[0];
if (inc != null)
{
this.processInclude(dec, inc, into);
}
else if (into != null)
{
into.config = node;
}
return into;
};
// Returns the codec into the registry
return codec;
}());
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
mxCodecRegistry.register(function()
{
/**
* Class: mxEditorCodec
*
* Codec for <mxEditor>s. This class is created and registered
* dynamically at load time and used implicitely via <mxCodec>
* and the <mxCodecRegistry>.
*
* Transient Fields:
*
* - modified
* - lastSnapshot
* - ignoredChanges
* - undoManager
* - graphContainer
* - toolbarContainer
*/
var codec = new mxObjectCodec(new mxEditor(),
['modified', 'lastSnapshot', 'ignoredChanges',
'undoManager', 'graphContainer', 'toolbarContainer']);
/**
* Function: beforeDecode
*
* Decodes the ui-part of the configuration node by reading
* a sequence of the following child nodes and attributes
* and passes the control to the default decoding mechanism:
*
* Child Nodes:
*
* stylesheet - Adds a CSS stylesheet to the document.
* resource - Adds the basename of a resource bundle.
* add - Creates or configures a known UI element.
*
* These elements may appear in any order given that the
* graph UI element is added before the toolbar element
* (see Known Keys).
*
* Attributes:
*
* as - Key for the UI element (see below).
* element - ID for the element in the document.
* style - CSS style to be used for the element or window.
* x - X coordinate for the new window.
* y - Y coordinate for the new window.
* width - Width for the new window.
* height - Optional height for the new window.
* name - Name of the stylesheet (absolute/relative URL).
* basename - Basename of the resource bundle (see <mxResources>).
*
* The x, y, width and height attributes are used to create a new
* <mxWindow> if the element attribute is not specified in an add
* node. The name and basename are only used in the stylesheet and
* resource nodes, respectively.
*
* Known Keys:
*
* graph - Main graph element (see <mxEditor.setGraphContainer>).
* title - Title element (see <mxEditor.setTitleContainer>).
* toolbar - Toolbar element (see <mxEditor.setToolbarContainer>).
* status - Status bar element (see <mxEditor.setStatusContainer>).
*
* Example:
*
* (code)
* <ui>
* <stylesheet name="css/process.css"/>
* <resource basename="resources/app"/>
* <add as="graph" element="graph"
* style="left:70px;right:20px;top:20px;bottom:40px"/>
* <add as="status" element="status"/>
* <add as="toolbar" x="10" y="20" width="54"/>
* </ui>
* (end)
*/
codec.afterDecode = function(dec, node, obj)
{
// Assigns the specified templates for edges
var defaultEdge = node.getAttribute('defaultEdge');
if (defaultEdge != null)
{
node.removeAttribute('defaultEdge');
obj.defaultEdge = obj.templates[defaultEdge];
}
// Assigns the specified templates for groups
var defaultGroup = node.getAttribute('defaultGroup');
if (defaultGroup != null)
{
node.removeAttribute('defaultGroup');
obj.defaultGroup = obj.templates[defaultGroup];
}
return obj;
};
/**
* Function: decodeChild
*
* Overrides decode child to handle special child nodes.
*/
codec.decodeChild = function(dec, child, obj)
{
if (child.nodeName == 'Array')
{
var role = child.getAttribute('as');
if (role == 'templates')
{
this.decodeTemplates(dec, child, obj);
return;
}
}
else if (child.nodeName == 'ui')
{
this.decodeUi(dec, child, obj);
return;
}
mxObjectCodec.prototype.decodeChild.apply(this, arguments);
};
/**
* Function: decodeTemplates
*
* Decodes the cells from the given node as templates.
*/
codec.decodeUi = function(dec, node, editor)
{
var tmp = node.firstChild;
while (tmp != null)
{
if (tmp.nodeName == 'add')
{
var as = tmp.getAttribute('as');
var elt = tmp.getAttribute('element');
var style = tmp.getAttribute('style');
var element = null;
if (elt != null)
{
element = document.getElementById(elt);
if (element != null && style != null)
{
element.style.cssText += ';' + style;
}
}
else
{
var x = parseInt(tmp.getAttribute('x'));
var y = parseInt(tmp.getAttribute('y'));
var width = tmp.getAttribute('width');
var height = tmp.getAttribute('height');
// Creates a new window around the element
element = document.createElement('div');
element.style.cssText = style;
var wnd = new mxWindow(mxResources.get(as) || as,
element, x, y, width, height, false, true);
wnd.setVisible(true);
}
// TODO: Make more generic
if (as == 'graph')
{
editor.setGraphContainer(element);
}
else if (as == 'toolbar')
{
editor.setToolbarContainer(element);
}
else if (as == 'title')
{
editor.setTitleContainer(element);
}
else if (as == 'status')
{
editor.setStatusContainer(element);
}
else if (as == 'map')
{
editor.setMapContainer(element);
}
}
else if (tmp.nodeName == 'resource')
{
mxResources.add(tmp.getAttribute('basename'));
}
else if (tmp.nodeName == 'stylesheet')
{
mxClient.link('stylesheet', tmp.getAttribute('name'));
}
tmp = tmp.nextSibling;
}
};
/**
* Function: decodeTemplates
*
* Decodes the cells from the given node as templates.
*/
codec.decodeTemplates = function(dec, node, editor)
{
if (editor.templates == null)
{
editor.templates = [];
}
var children = mxUtils.getChildNodes(node);
for (var j=0; j<children.length; j++)
{
var name = children[j].getAttribute('as');
var child = children[j].firstChild;
while (child != null && child.nodeType != 1)
{
child = child.nextSibling;
}
if (child != null)
{
// LATER: Only single cells means you need
// to group multiple cells within another
// cell. This should be changed to support
// arrays of cells, or the wrapper must
// be automatically handled in this class.
editor.templates[name] = dec.decodeCell(child);
}
}
};
// Returns the codec into the registry
return codec;
}());