91115 lines
2.1 MiB
91115 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.1.1.
|
|
*/
|
|
VERSION: '4.1.1',
|
|
|
|
/**
|
|
* 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 != null && navigator.userAgent.indexOf('MSIE') >= 0,
|
|
|
|
/**
|
|
* Variable: IS_IE6
|
|
*
|
|
* True if the current browser is Internet Explorer 6.x.
|
|
*/
|
|
IS_IE6: navigator.userAgent != null && navigator.userAgent.indexOf('MSIE 6') >= 0,
|
|
|
|
/**
|
|
* Variable: IS_IE11
|
|
*
|
|
* True if the current browser is Internet Explorer 11.x.
|
|
*/
|
|
IS_IE11: navigator.userAgent != null && !!navigator.userAgent.match(/Trident\/7\./),
|
|
|
|
/**
|
|
* Variable: IS_EDGE
|
|
*
|
|
* True if the current browser is Microsoft Edge.
|
|
*/
|
|
IS_EDGE: navigator.userAgent != null && !!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 != null && 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 != null &&
|
|
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 != null &&
|
|
(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 != null &&
|
|
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: /constructor/i.test(window.HTMLElement) || (function (p) {
|
|
return p.toString() === "[object SafariRemoteNotification]";
|
|
})(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)),
|
|
|
|
/**
|
|
* Variable: IS_ANDROID
|
|
*
|
|
* Returns true if the user agent contains Android.
|
|
*/
|
|
IS_ANDROID: navigator.appVersion.indexOf('Android') >= 0,
|
|
|
|
/**
|
|
* Variable: IS_IOS
|
|
*
|
|
* Returns true if the user agent is an iPad, iPhone or iPod.
|
|
*/
|
|
IS_IOS: (/iP(hone|od|ad)/.test(navigator.platform)),
|
|
|
|
/**
|
|
* Variable: IOS_VERSION
|
|
*
|
|
* Returns the major version number for iOS devices or 0 if the
|
|
* device is not an iOS device.
|
|
*/
|
|
IOS_VERSION: (function()
|
|
{
|
|
if ((/iP(hone|od|ad)/.test(navigator.platform)))
|
|
{
|
|
var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
|
|
|
|
if (v != null && v.length > 0)
|
|
{
|
|
return parseInt(v[1]);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
})(),
|
|
|
|
/**
|
|
* Variable: IS_GC
|
|
*
|
|
* True if the current browser is Google Chrome.
|
|
*/
|
|
IS_GC: /Google Inc/.test(navigator.vendor),
|
|
|
|
/**
|
|
* 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: typeof InstallTrigger !== 'undefined',
|
|
|
|
/**
|
|
* 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.appVersion),
|
|
|
|
/**
|
|
* 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>');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Detects desktop mode on iPad Pro which should block event handling like iOS 12.
|
|
*/
|
|
if (mxClient.IS_SF && mxClient.IS_TOUCH && !mxClient.IS_IOS)
|
|
{
|
|
mxClient.IOS_VERSION = 13;
|
|
mxClient.IOS = true;
|
|
}
|
|
|
|
/**
|
|
* 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, not setAttribute.
|
|
if (document.namespaces != null)
|
|
{
|
|
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 != null &&
|
|
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" (ö).
|
|
*
|
|
* 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))
|
|
{
|
|
return mxUtils.importNodeImplementation(doc, node, allChildren);
|
|
}
|
|
else
|
|
{
|
|
return doc.importNode(node, allChildren);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: importNodeImplementation
|
|
*
|
|
* Full DOM API implementation for importNode without using importNode API call.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* doc - Document to import the node into.
|
|
* node - Node to be imported.
|
|
* allChildren - If all children should be imported.
|
|
*/
|
|
importNodeImplementation: function(doc, node, allChildren)
|
|
{
|
|
switch (node.nodeType)
|
|
{
|
|
case 1: /* element */
|
|
{
|
|
var newNode = doc.createElement(node.nodeName);
|
|
|
|
if (node.attributes && node.attributes.length > 0)
|
|
{
|
|
for (var i = 0; i < node.attributes.length; i++)
|
|
{
|
|
newNode.setAttribute(node.attributes[i].nodeName,
|
|
node.getAttribute(node.attributes[i].nodeName));
|
|
}
|
|
}
|
|
|
|
if (allChildren && node.childNodes && node.childNodes.length > 0)
|
|
{
|
|
for (var i = 0; i < node.childNodes.length; i++)
|
|
{
|
|
newNode.appendChild(mxUtils.importNodeImplementation(doc, node.childNodes[i], allChildren));
|
|
}
|
|
}
|
|
|
|
return newNode;
|
|
break;
|
|
}
|
|
case 3: /* text */
|
|
case 4: /* cdata-section */
|
|
case 8: /* comment */
|
|
{
|
|
return doc.createTextNode((node.nodeValue != null) ? node.nodeValue : node.value);
|
|
break;
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Function: createXmlDocument
|
|
*
|
|
* Returns a new, empty XML document.
|
|
*/
|
|
createXmlDocument: function()
|
|
{
|
|
var doc = null;
|
|
|
|
if (document.implementation && document.implementation.createDocument)
|
|
{
|
|
doc = document.implementation.createDocument('', '', null);
|
|
}
|
|
else if ("ActiveXObject" in window)
|
|
{
|
|
doc = mxUtils.createMsXmlDocument();
|
|
}
|
|
|
|
return doc;
|
|
},
|
|
|
|
/**
|
|
* Function: createMsXmlDocument
|
|
*
|
|
* Returns a new, empty Microsoft.XMLDOM document using ActiveXObject.
|
|
*/
|
|
createMsXmlDocument: function()
|
|
{
|
|
var doc = new ActiveXObject('Microsoft.XMLDOM');
|
|
doc.async = false;
|
|
|
|
// Workaround for parsing errors with SVG DTD
|
|
doc.validateOnParse = false;
|
|
doc.resolveExternals = false;
|
|
|
|
return doc;
|
|
},
|
|
|
|
/**
|
|
* Function: parseXml
|
|
*
|
|
* Parses the specified XML string into a new XML document and returns the
|
|
* new document.
|
|
*
|
|
* Example:
|
|
*
|
|
* (code)
|
|
* var doc = mxUtils.parseXml(
|
|
* '<mxGraphModel><root><MyDiagram id="0"><mxCell/></MyDiagram>'+
|
|
* '<MyLayer id="1"><mxCell parent="0" /></MyLayer><MyObject id="2">'+
|
|
* '<mxCell style="strokeColor=blue;fillColor=red" parent="1" vertex="1">'+
|
|
* '<mxGeometry x="10" y="10" width="80" height="30" as="geometry"/>'+
|
|
* '</mxCell></MyObject></root></mxGraphModel>');
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* xml - String that contains the XML data.
|
|
*/
|
|
parseXml: function()
|
|
{
|
|
if (window.DOMParser)
|
|
{
|
|
return function(xml)
|
|
{
|
|
var parser = new DOMParser();
|
|
|
|
return parser.parseFromString(xml, 'text/xml');
|
|
};
|
|
}
|
|
else // IE<=9
|
|
{
|
|
return function(xml)
|
|
{
|
|
var doc = mxUtils.createMsXmlDocument();
|
|
doc.loadXML(xml);
|
|
|
|
return doc;
|
|
};
|
|
}
|
|
}(),
|
|
|
|
/**
|
|
* Function: clearSelection
|
|
*
|
|
* Clears the current selection in the page.
|
|
*/
|
|
clearSelection: function()
|
|
{
|
|
if (document.selection)
|
|
{
|
|
return function()
|
|
{
|
|
document.selection.empty();
|
|
};
|
|
}
|
|
else if (window.getSelection)
|
|
{
|
|
return function()
|
|
{
|
|
if (window.getSelection().empty)
|
|
{
|
|
window.getSelection().empty();
|
|
}
|
|
else if (window.getSelection().removeAllRanges)
|
|
{
|
|
window.getSelection().removeAllRanges();
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return function() { };
|
|
}
|
|
}(),
|
|
|
|
/**
|
|
* Function: removeWhitespace
|
|
*
|
|
* Removes the sibling text nodes for the given node that only consists
|
|
* of tabs, newlines and spaces.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node whose siblings should be removed.
|
|
* before - Optional boolean that specifies the direction of the traversal.
|
|
*/
|
|
removeWhitespace: function(node, before)
|
|
{
|
|
var tmp = (before) ? node.previousSibling : node.nextSibling;
|
|
|
|
while (tmp != null && tmp.nodeType == mxConstants.NODETYPE_TEXT)
|
|
{
|
|
var next = (before) ? tmp.previousSibling : tmp.nextSibling;
|
|
var text = mxUtils.getTextContent(tmp);
|
|
|
|
if (mxUtils.trim(text).length == 0)
|
|
{
|
|
tmp.parentNode.removeChild(tmp);
|
|
}
|
|
|
|
tmp = next;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: htmlEntities
|
|
*
|
|
* Replaces characters (less than, greater than, newlines and quotes) with
|
|
* their HTML entities in the given string and returns the result.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* s - String that contains the characters to be converted.
|
|
* newline - If newlines should be replaced. Default is true.
|
|
*/
|
|
htmlEntities: function(s, newline)
|
|
{
|
|
s = String(s || '');
|
|
|
|
s = s.replace(/&/g,'&'); // 38 26
|
|
s = s.replace(/"/g,'"'); // 34 22
|
|
s = s.replace(/\'/g,'''); // 39 27
|
|
s = s.replace(/</g,'<'); // 60 3C
|
|
s = s.replace(/>/g,'>'); // 62 3E
|
|
|
|
if (newline == null || newline)
|
|
{
|
|
s = s.replace(/\n/g, '
');
|
|
}
|
|
|
|
return s;
|
|
},
|
|
|
|
/**
|
|
* Function: isVml
|
|
*
|
|
* Returns true if the given node is in the VML namespace.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node whose tag urn should be checked.
|
|
*/
|
|
isVml: function(node)
|
|
{
|
|
return node != null && node.tagUrn == 'urn:schemas-microsoft-com:vml';
|
|
},
|
|
|
|
/**
|
|
* Function: getXml
|
|
*
|
|
* Returns the XML content of the specified node. For Internet Explorer,
|
|
* all \r\n\t[\t]* are removed from the XML string and the remaining \r\n
|
|
* are replaced by \n. All \n are then replaced with linefeed, or 
 if
|
|
* no linefeed is defined.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to return the XML for.
|
|
* linefeed - Optional string that linefeeds are converted into. Default is
|
|
* 

|
|
*/
|
|
getXml: function(node, linefeed)
|
|
{
|
|
var xml = '';
|
|
|
|
if (mxClient.IS_IE || mxClient.IS_IE11)
|
|
{
|
|
xml = mxUtils.getPrettyXml(node, '', '', '');
|
|
}
|
|
else if (window.XMLSerializer != null)
|
|
{
|
|
var xmlSerializer = new XMLSerializer();
|
|
xml = xmlSerializer.serializeToString(node);
|
|
}
|
|
else if (node.xml != null)
|
|
{
|
|
xml = node.xml.replace(/\r\n\t[\t]*/g, '').
|
|
replace(/>\r\n/g, '>').
|
|
replace(/\r\n/g, '\n');
|
|
}
|
|
|
|
// Replaces linefeeds with HTML Entities.
|
|
linefeed = linefeed || '
';
|
|
xml = xml.replace(/\n/g, linefeed);
|
|
|
|
return xml;
|
|
},
|
|
|
|
/**
|
|
* Function: getPrettyXML
|
|
*
|
|
* Returns a pretty printed string that represents the XML tree for the
|
|
* given node. This method should only be used to print XML for reading,
|
|
* use <getXml> instead to obtain a string for processing.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to return the XML for.
|
|
* tab - Optional string that specifies the indentation for one level.
|
|
* Default is two spaces.
|
|
* indent - Optional string that represents the current indentation.
|
|
* Default is an empty string.
|
|
* newline - Option string that represents a linefeed. Default is '\n'.
|
|
*/
|
|
getPrettyXml: function(node, tab, indent, newline, ns)
|
|
{
|
|
var result = [];
|
|
|
|
if (node != null)
|
|
{
|
|
tab = (tab != null) ? tab : ' ';
|
|
indent = (indent != null) ? indent : '';
|
|
newline = (newline != null) ? newline : '\n';
|
|
|
|
if (node.namespaceURI != null && node.namespaceURI != ns)
|
|
{
|
|
ns = node.namespaceURI;
|
|
|
|
if (node.getAttribute('xmlns') == null)
|
|
{
|
|
node.setAttribute('xmlns', node.namespaceURI);
|
|
}
|
|
}
|
|
|
|
if (node.nodeType == mxConstants.NODETYPE_DOCUMENT)
|
|
{
|
|
result.push(mxUtils.getPrettyXml(node.documentElement, tab, indent, newline, ns));
|
|
}
|
|
else if (node.nodeType == mxConstants.NODETYPE_DOCUMENT_FRAGMENT)
|
|
{
|
|
var tmp = node.firstChild;
|
|
|
|
if (tmp != null)
|
|
{
|
|
while (tmp != null)
|
|
{
|
|
result.push(mxUtils.getPrettyXml(tmp, tab, indent, newline, ns));
|
|
tmp = tmp.nextSibling;
|
|
}
|
|
}
|
|
}
|
|
else if (node.nodeType == mxConstants.NODETYPE_COMMENT)
|
|
{
|
|
var value = mxUtils.getTextContent(node);
|
|
|
|
if (value.length > 0)
|
|
{
|
|
result.push(indent + '<!--' + value + '-->' + newline);
|
|
}
|
|
}
|
|
else if (node.nodeType == mxConstants.NODETYPE_TEXT)
|
|
{
|
|
var value = mxUtils.getTextContent(node);
|
|
|
|
if (value.length > 0)
|
|
{
|
|
result.push(indent + mxUtils.htmlEntities(mxUtils.trim(value), false) + newline);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.push(indent + '<' + node.nodeName);
|
|
|
|
// Creates the string with the node attributes
|
|
// and converts all HTML entities in the values
|
|
var attrs = node.attributes;
|
|
|
|
if (attrs != null)
|
|
{
|
|
for (var i = 0; i < attrs.length; i++)
|
|
{
|
|
var val = mxUtils.htmlEntities(attrs[i].value);
|
|
result.push(' ' + attrs[i].nodeName + '="' + val + '"');
|
|
}
|
|
}
|
|
|
|
// Recursively creates the XML string for each child
|
|
// node and appends it here with an indentation
|
|
var tmp = node.firstChild;
|
|
|
|
if (tmp != null)
|
|
{
|
|
result.push('>' + newline);
|
|
|
|
while (tmp != null)
|
|
{
|
|
result.push(mxUtils.getPrettyXml(tmp, tab, indent + tab, newline, ns));
|
|
tmp = tmp.nextSibling;
|
|
}
|
|
|
|
result.push(indent + '</'+ node.nodeName + '>' + newline);
|
|
}
|
|
else
|
|
{
|
|
result.push(' />' + newline);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.join('');
|
|
},
|
|
|
|
/**
|
|
* Function: extractTextWithWhitespace
|
|
*
|
|
* Returns the text content of the specified node.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* elems - DOM nodes to return the text for.
|
|
*/
|
|
extractTextWithWhitespace: function(elems)
|
|
{
|
|
// Known block elements for handling linefeeds (list is not complete)
|
|
var blocks = ['BLOCKQUOTE', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'OL', 'P', 'PRE', 'TABLE', 'UL'];
|
|
var ret = [];
|
|
|
|
function doExtract(elts)
|
|
{
|
|
// Single break should be ignored
|
|
if (elts.length == 1 && (elts[0].nodeName == 'BR' ||
|
|
elts[0].innerHTML == '\n'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < elts.length; i++)
|
|
{
|
|
var elem = elts[i];
|
|
|
|
// DIV with a br or linefeed forces a linefeed
|
|
if (elem.nodeName == 'BR' || elem.innerHTML == '\n' ||
|
|
((elts.length == 1 || i == 0) && (elem.nodeName == 'DIV' &&
|
|
elem.innerHTML.toLowerCase() == '<br>')))
|
|
{
|
|
ret.push('\n');
|
|
}
|
|
else
|
|
{
|
|
if (elem.nodeType === 3 || elem.nodeType === 4)
|
|
{
|
|
if (elem.nodeValue.length > 0)
|
|
{
|
|
ret.push(elem.nodeValue);
|
|
}
|
|
}
|
|
else if (elem.nodeType !== 8 && elem.childNodes.length > 0)
|
|
{
|
|
doExtract(elem.childNodes);
|
|
}
|
|
|
|
if (i < elts.length - 1 && mxUtils.indexOf(blocks, elts[i + 1].nodeName) >= 0)
|
|
{
|
|
ret.push('\n');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
doExtract(elems);
|
|
|
|
return ret.join('');
|
|
},
|
|
|
|
/**
|
|
* Function: replaceTrailingNewlines
|
|
*
|
|
* Replaces each trailing newline with the given pattern.
|
|
*/
|
|
replaceTrailingNewlines: function(str, pattern)
|
|
{
|
|
// LATER: Check is this can be done with a regular expression
|
|
var postfix = '';
|
|
|
|
while (str.length > 0 && str.charAt(str.length - 1) == '\n')
|
|
{
|
|
str = str.substring(0, str.length - 1);
|
|
postfix += pattern;
|
|
}
|
|
|
|
return str + postfix;
|
|
},
|
|
|
|
/**
|
|
* Function: getTextContent
|
|
*
|
|
* Returns the text content of the specified node.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to return the text content for.
|
|
*/
|
|
getTextContent: function(node)
|
|
{
|
|
// Only IE10-
|
|
if (mxClient.IS_IE && node.innerText !== undefined)
|
|
{
|
|
return node.innerText;
|
|
}
|
|
else
|
|
{
|
|
return (node != null) ? node[(node.textContent === undefined) ? 'text' : 'textContent'] : '';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: setTextContent
|
|
*
|
|
* Sets the text content of the specified node.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to set the text content for.
|
|
* text - String that represents the text content.
|
|
*/
|
|
setTextContent: function(node, text)
|
|
{
|
|
if (node.innerText !== undefined)
|
|
{
|
|
node.innerText = text;
|
|
}
|
|
else
|
|
{
|
|
node[(node.textContent === undefined) ? 'text' : 'textContent'] = text;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: getInnerHtml
|
|
*
|
|
* Returns the inner HTML for the given node as a string or an empty string
|
|
* if no node was specified. The inner HTML is the text representing all
|
|
* children of the node, but not the node itself.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to return the inner HTML for.
|
|
*/
|
|
getInnerHtml: function()
|
|
{
|
|
if (mxClient.IS_IE)
|
|
{
|
|
return function(node)
|
|
{
|
|
if (node != null)
|
|
{
|
|
return node.innerHTML;
|
|
}
|
|
|
|
return '';
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return function(node)
|
|
{
|
|
if (node != null)
|
|
{
|
|
var serializer = new XMLSerializer();
|
|
return serializer.serializeToString(node);
|
|
}
|
|
|
|
return '';
|
|
};
|
|
}
|
|
}(),
|
|
|
|
/**
|
|
* Function: getOuterHtml
|
|
*
|
|
* Returns the outer HTML for the given node as a string or an empty
|
|
* string if no node was specified. The outer HTML is the text representing
|
|
* all children of the node including the node itself.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* node - DOM node to return the outer HTML for.
|
|
*/
|
|
getOuterHtml: function()
|
|
{
|
|
if (mxClient.IS_IE)
|
|
{
|
|
return function(node)
|
|
{
|
|
if (node != null)
|
|
{
|
|
if (node.outerHTML != null)
|
|
{
|
|
return node.outerHTML;
|
|
}
|
|
else
|
|
{
|
|
var tmp = [];
|
|
tmp.push('<'+node.nodeName);
|
|
|
|
var attrs = node.attributes;
|
|
|
|
if (attrs != null)
|
|
{
|
|
for (var i = 0; i < attrs.length; i++)
|
|
{
|
|
var value = attrs[i].value;
|
|
|
|
if (value != null && value.length > 0)
|
|
{
|
|
tmp.push(' ');
|
|
tmp.push(attrs[i].nodeName);
|
|
tmp.push('="');
|
|
tmp.push(value);
|
|
tmp.push('"');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.innerHTML.length == 0)
|
|
{
|
|
tmp.push('/>');
|
|
}
|
|
else
|
|
{
|
|
tmp.push('>');
|
|
tmp.push(node.innerHTML);
|
|
tmp.push('</'+node.nodeName+'>');
|
|
}
|
|
|
|
return tmp.join('');
|
|
}
|
|
}
|
|
|
|
return '';
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return function(node)
|
|
{
|
|
if (node != null)
|
|
{
|
|
var serializer = new XMLSerializer();
|
|
return serializer.serializeToString(node);
|
|
}
|
|
|
|
return '';
|
|
};
|
|
}
|
|
}(),
|
|
|
|
/**
|
|
* Function: write
|
|
*
|
|
* Creates a text node for the given string and appends it to the given
|
|
* parent. Returns the text node.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to append the text node to.
|
|
* text - String representing the text to be added.
|
|
*/
|
|
write: function(parent, text)
|
|
{
|
|
var doc = parent.ownerDocument;
|
|
var node = doc.createTextNode(text);
|
|
|
|
if (parent != null)
|
|
{
|
|
parent.appendChild(node);
|
|
}
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Function: writeln
|
|
*
|
|
* Creates a text node for the given string and appends it to the given
|
|
* parent with an additional linefeed. Returns the text node.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to append the text node to.
|
|
* text - String representing the text to be added.
|
|
*/
|
|
writeln: function(parent, text)
|
|
{
|
|
var doc = parent.ownerDocument;
|
|
var node = doc.createTextNode(text);
|
|
|
|
if (parent != null)
|
|
{
|
|
parent.appendChild(node);
|
|
parent.appendChild(document.createElement('br'));
|
|
}
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Function: br
|
|
*
|
|
* Appends a linebreak to the given parent and returns the linebreak.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to append the linebreak to.
|
|
*/
|
|
br: function(parent, count)
|
|
{
|
|
count = count || 1;
|
|
var br = null;
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
if (parent != null)
|
|
{
|
|
br = parent.ownerDocument.createElement('br');
|
|
parent.appendChild(br);
|
|
}
|
|
}
|
|
|
|
return br;
|
|
},
|
|
|
|
/**
|
|
* Function: button
|
|
*
|
|
* Returns a new button with the given level and function as an onclick
|
|
* event handler.
|
|
*
|
|
* (code)
|
|
* document.body.appendChild(mxUtils.button('Test', function(evt)
|
|
* {
|
|
* alert('Hello, World!');
|
|
* }));
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* label - String that represents the label of the button.
|
|
* funct - Function to be called if the button is pressed.
|
|
* doc - Optional document to be used for creating the button. Default is the
|
|
* current document.
|
|
*/
|
|
button: function(label, funct, doc)
|
|
{
|
|
doc = (doc != null) ? doc : document;
|
|
|
|
var button = doc.createElement('button');
|
|
mxUtils.write(button, label);
|
|
|
|
mxEvent.addListener(button, 'click', function(evt)
|
|
{
|
|
funct(evt);
|
|
});
|
|
|
|
return button;
|
|
},
|
|
|
|
/**
|
|
* Function: para
|
|
*
|
|
* Appends a new paragraph with the given text to the specified parent and
|
|
* returns the paragraph.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to append the text node to.
|
|
* text - String representing the text for the new paragraph.
|
|
*/
|
|
para: function(parent, text)
|
|
{
|
|
var p = document.createElement('p');
|
|
mxUtils.write(p, text);
|
|
|
|
if (parent != null)
|
|
{
|
|
parent.appendChild(p);
|
|
}
|
|
|
|
return p;
|
|
},
|
|
|
|
/**
|
|
* Function: addTransparentBackgroundFilter
|
|
*
|
|
* Adds a transparent background to the filter of the given node. This
|
|
* background can be used in IE8 standards mode (native IE8 only) to pass
|
|
* events through the node.
|
|
*/
|
|
addTransparentBackgroundFilter: function(node)
|
|
{
|
|
node.style.filter += 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'' +
|
|
mxClient.imageBasePath + '/transparent.gif\', sizingMethod=\'scale\')';
|
|
},
|
|
|
|
/**
|
|
* Function: linkAction
|
|
*
|
|
* Adds a hyperlink to the specified parent that invokes action on the
|
|
* specified editor.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to contain the new link.
|
|
* text - String that is used as the link label.
|
|
* editor - <mxEditor> that will execute the action.
|
|
* action - String that defines the name of the action to be executed.
|
|
* pad - Optional left-padding for the link. Default is 0.
|
|
*/
|
|
linkAction: function(parent, text, editor, action, pad)
|
|
{
|
|
return mxUtils.link(parent, text, function()
|
|
{
|
|
editor.execute(action);
|
|
}, pad);
|
|
},
|
|
|
|
/**
|
|
* Function: linkInvoke
|
|
*
|
|
* Adds a hyperlink to the specified parent that invokes the specified
|
|
* function on the editor passing along the specified argument. The
|
|
* function name is the name of a function of the editor instance,
|
|
* not an action name.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to contain the new link.
|
|
* text - String that is used as the link label.
|
|
* editor - <mxEditor> instance to execute the function on.
|
|
* functName - String that represents the name of the function.
|
|
* arg - Object that represents the argument to the function.
|
|
* pad - Optional left-padding for the link. Default is 0.
|
|
*/
|
|
linkInvoke: function(parent, text, editor, functName, arg, pad)
|
|
{
|
|
return mxUtils.link(parent, text, function()
|
|
{
|
|
editor[functName](arg);
|
|
}, pad);
|
|
},
|
|
|
|
/**
|
|
* Function: link
|
|
*
|
|
* Adds a hyperlink to the specified parent and invokes the given function
|
|
* when the link is clicked.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* parent - DOM node to contain the new link.
|
|
* text - String that is used as the link label.
|
|
* funct - Function to execute when the link is clicked.
|
|
* pad - Optional left-padding for the link. Default is 0.
|
|
*/
|
|
link: function(parent, text, funct, pad)
|
|
{
|
|
var a = document.createElement('span');
|
|
|
|
a.style.color = 'blue';
|
|
a.style.textDecoration = 'underline';
|
|
a.style.cursor = 'pointer';
|
|
|
|
if (pad != null)
|
|
{
|
|
a.style.paddingLeft = pad+'px';
|
|
}
|
|
|
|
mxEvent.addListener(a, 'click', funct);
|
|
mxUtils.write(a, text);
|
|
|
|
if (parent != null)
|
|
{
|
|
parent.appendChild(a);
|
|
}
|
|
|
|
return a;
|
|
},
|
|
|
|
/**
|
|
* Function: getDocumentSize
|
|
*
|
|
* Returns the client size for the current document as an <mxRectangle>.
|
|
*/
|
|
getDocumentSize: function()
|
|
{
|
|
var b = document.body;
|
|
var d = document.documentElement;
|
|
|
|
try
|
|
{
|
|
return new mxRectangle(0, 0, b.clientWidth || d.clientWidth, Math.max(b.clientHeight || 0, d.clientHeight));
|
|
}
|
|
catch (e)
|
|
{
|
|
return new mxRectangle();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: fit
|
|
*
|
|
* Makes sure the given node is inside the visible area of the window. This
|
|
* is done by setting the left and top in the style.
|
|
*/
|
|
fit: function(node)
|
|
{
|
|
var ds = mxUtils.getDocumentSize();
|
|
var left = parseInt(node.offsetLeft);
|
|
var width = parseInt(node.offsetWidth);
|
|
|
|
var offset = mxUtils.getDocumentScrollOrigin(node.ownerDocument);
|
|
var sl = offset.x;
|
|
var st = offset.y;
|
|
|
|
var b = document.body;
|
|
var d = document.documentElement;
|
|
var right = (sl) + ds.width;
|
|
|
|
if (left + width > right)
|
|
{
|
|
node.style.left = Math.max(sl, right - width) + 'px';
|
|
}
|
|
|
|
var top = parseInt(node.offsetTop);
|
|
var height = parseInt(node.offsetHeight);
|
|
|
|
var bottom = st + ds.height;
|
|
|
|
if (top + height > bottom)
|
|
{
|
|
node.style.top = Math.max(st, bottom - height) + 'px';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: load
|
|
*
|
|
* Loads the specified URL *synchronously* and returns the <mxXmlRequest>.
|
|
* Throws an exception if the file cannot be loaded. See <mxUtils.get> for
|
|
* an asynchronous implementation.
|
|
*
|
|
* Example:
|
|
*
|
|
* (code)
|
|
* try
|
|
* {
|
|
* var req = mxUtils.load(filename);
|
|
* var root = req.getDocumentElement();
|
|
* // Process XML DOM...
|
|
* }
|
|
* catch (ex)
|
|
* {
|
|
* mxUtils.alert('Cannot load '+filename+': '+ex);
|
|
* }
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* url - URL to get the data from.
|
|
*/
|
|
load: function(url)
|
|
{
|
|
var req = new mxXmlRequest(url, null, 'GET', false);
|
|
req.send();
|
|
|
|
return req;
|
|
},
|
|
|
|
/**
|
|
* Function: get
|
|
*
|
|
* Loads the specified URL *asynchronously* and invokes the given functions
|
|
* depending on the request status. Returns the <mxXmlRequest> in use. Both
|
|
* functions take the <mxXmlRequest> as the only parameter. See
|
|
* <mxUtils.load> for a synchronous implementation.
|
|
*
|
|
* Example:
|
|
*
|
|
* (code)
|
|
* mxUtils.get(url, function(req)
|
|
* {
|
|
* var node = req.getDocumentElement();
|
|
* // Process XML DOM...
|
|
* });
|
|
* (end)
|
|
*
|
|
* So for example, to load a diagram into an existing graph model, the
|
|
* following code is used.
|
|
*
|
|
* (code)
|
|
* mxUtils.get(url, function(req)
|
|
* {
|
|
* var node = req.getDocumentElement();
|
|
* var dec = new mxCodec(node.ownerDocument);
|
|
* dec.decode(node, graph.getModel());
|
|
* });
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* url - URL to get the data from.
|
|
* onload - Optional function to execute for a successful response.
|
|
* onerror - Optional function to execute on error.
|
|
* binary - Optional boolean parameter that specifies if the request is
|
|
* binary.
|
|
* timeout - Optional timeout in ms before calling ontimeout.
|
|
* ontimeout - Optional function to execute on timeout.
|
|
* headers - Optional with headers, eg. {'Authorization': 'token xyz'}
|
|
*/
|
|
get: function(url, onload, onerror, binary, timeout, ontimeout, headers)
|
|
{
|
|
var req = new mxXmlRequest(url, null, 'GET');
|
|
var setRequestHeaders = req.setRequestHeaders;
|
|
|
|
if (headers)
|
|
{
|
|
req.setRequestHeaders = function(request, params)
|
|
{
|
|
setRequestHeaders.apply(this, arguments);
|
|
|
|
for (var key in headers)
|
|
{
|
|
request.setRequestHeader(key, headers[key]);
|
|
}
|
|
};
|
|
}
|
|
|
|
if (binary != null)
|
|
{
|
|
req.setBinary(binary);
|
|
}
|
|
|
|
req.send(onload, onerror, timeout, ontimeout);
|
|
|
|
return req;
|
|
},
|
|
|
|
/**
|
|
* Function: getAll
|
|
*
|
|
* Loads the URLs in the given array *asynchronously* and invokes the given function
|
|
* if all requests returned with a valid 2xx status. The error handler is invoked
|
|
* once on the first error or invalid response.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* urls - Array of URLs to be loaded.
|
|
* onload - Callback with array of <mxXmlRequests>.
|
|
* onerror - Optional function to execute on error.
|
|
*/
|
|
getAll: function(urls, onload, onerror)
|
|
{
|
|
var remain = urls.length;
|
|
var result = [];
|
|
var errors = 0;
|
|
var err = function()
|
|
{
|
|
if (errors == 0 && onerror != null)
|
|
{
|
|
onerror();
|
|
}
|
|
|
|
errors++;
|
|
};
|
|
|
|
for (var i = 0; i < urls.length; i++)
|
|
{
|
|
(function(url, index)
|
|
{
|
|
mxUtils.get(url, function(req)
|
|
{
|
|
var status = req.getStatus();
|
|
|
|
if (status < 200 || status > 299)
|
|
{
|
|
err();
|
|
}
|
|
else
|
|
{
|
|
result[index] = req;
|
|
remain--;
|
|
|
|
if (remain == 0)
|
|
{
|
|
onload(result);
|
|
}
|
|
}
|
|
}, err);
|
|
})(urls[i], i);
|
|
}
|
|
|
|
if (remain == 0)
|
|
{
|
|
onload(result);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function: post
|
|
*
|
|
* Posts the specified params to the given URL *asynchronously* and invokes
|
|
* the given functions depending on the request status. Returns the
|
|
* <mxXmlRequest> in use. Both functions take the <mxXmlRequest> as the
|
|
* only parameter. Make sure to use encodeURIComponent for the parameter
|
|
* values.
|
|
*
|
|
* Example:
|
|
*
|
|
* (code)
|
|
* mxUtils.post(url, 'key=value', function(req)
|
|
* {
|
|
* mxUtils.alert('Ready: '+req.isReady()+' Status: '+req.getStatus());
|
|
* // Process req.getDocumentElement() using DOM API if OK...
|
|
* });
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* url - URL to get the data from.
|
|
* params - Parameters for the post request.
|
|
* onload - Optional function to execute for a successful response.
|
|
* onerror - Optional function to execute on error.
|
|
*/
|
|
post: function(url, params, onload, onerror)
|
|
{
|
|
return new mxXmlRequest(url, params).send(onload, onerror);
|
|
},
|
|
|
|
/**
|
|
* Function: submit
|
|
*
|
|
* Submits the given parameters to the specified URL using
|
|
* <mxXmlRequest.simulate> and returns the <mxXmlRequest>.
|
|
* Make sure to use encodeURIComponent for the parameter
|
|
* values.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* url - URL to get the data from.
|
|
* params - Parameters for the form.
|
|
* doc - Document to create the form in.
|
|
* target - Target to send the form result to.
|
|
*/
|
|
submit: function(url, params, doc, target)
|
|
{
|
|
return new mxXmlRequest(url, params).simulate(doc, target);
|
|
},
|
|
|
|
/**
|
|
* Function: loadInto
|
|
*
|
|
* Loads the specified URL *asynchronously* into the specified document,
|
|
* invoking onload after the document has been loaded. This implementation
|
|
* does not use <mxXmlRequest>, but the document.load method.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* url - URL to get the data from.
|
|
* doc - The document to load the URL into.
|
|
* onload - Function to execute when the URL has been loaded.
|
|
*/
|
|
loadInto: function(url, doc, onload)
|
|
{
|
|
if (mxClient.IS_IE)
|
|
{
|
|
doc.onreadystatechange = function ()
|
|
{
|
|
if (doc.readyState == 4)
|
|
{
|
|
onload();
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
doc.addEventListener('load', onload, false);
|
|
}
|
|
|
|
doc.load(url);
|
|
},
|
|
|
|
/**
|
|
* Function: getValue
|
|
*
|
|
* Returns the value for the given key in the given associative array or
|
|
* the given default value if the value is null.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* array - Associative array that contains the value for the key.
|
|
* key - Key whose value should be returned.
|
|
* defaultValue - Value to be returned if the value for the given
|
|
* key is null.
|
|
*/
|
|
getValue: function(array, key, defaultValue)
|
|
{
|
|
var value = (array != null) ? array[key] : null;
|
|
|
|
if (value == null)
|
|
{
|
|
value = defaultValue;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Function: getNumber
|
|
*
|
|
* Returns the numeric value for the given key in the given associative
|
|
* array or the given default value (or 0) if the value is null. The value
|
|
* is converted to a numeric value using the Number function.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* array - Associative array that contains the value for the key.
|
|
* key - Key whose value should be returned.
|
|
* defaultValue - Value to be returned if the value for the given
|
|
* key is null. Default is 0.
|
|
*/
|
|
getNumber: function(array, key, defaultValue)
|
|
{
|
|
var value = (array != null) ? array[key] : null;
|
|
|
|
if (value == null)
|
|
{
|
|
value = defaultValue || 0;
|
|
}
|
|
|
|
return Number(value);
|
|
},
|
|
|
|
/**
|
|
* Function: getColor
|
|
*
|
|
* Returns the color value for the given key in the given associative
|
|
* array or the given default value if the value is null. If the value
|
|
* is <mxConstants.NONE> then null is returned.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* array - Associative array that contains the value for the key.
|
|
* key - Key whose value should be returned.
|
|
* defaultValue - Value to be returned if the value for the given
|
|
* key is null. Default is null.
|
|
*/
|
|
getColor: function(array, key, defaultValue)
|
|
{
|
|
var value = (array != null) ? array[key] : null;
|
|
|
|
if (value == null)
|
|
{
|
|
value = defaultValue;
|
|
}
|
|
else if (value == mxConstants.NONE)
|
|
{
|
|
value = null;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Function: clone
|
|
*
|
|
* Recursively clones the specified object ignoring all fieldnames in the
|
|
* given array of transient fields. <mxObjectIdentity.FIELD_NAME> is always
|
|
* ignored by this function.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* obj - Object to be cloned.
|
|
* transients - Optional array of strings representing the fieldname to be
|
|
* ignored.
|
|
* shallow - Optional boolean argument to specify if a shallow clone should
|
|
* be created, that is, one where all object references are not cloned or,
|
|
* in other words, one where only atomic (strings, numbers) values are
|
|
* cloned. Default is false.
|
|
*/
|
|
clone: function(obj, transients, shallow)
|
|
{
|
|
shallow = (shallow != null) ? shallow : false;
|
|
var clone = null;
|
|
|
|
if (obj != null && typeof(obj.constructor) == 'function')
|
|
{
|
|
clone = new obj.constructor();
|
|
|
|
for (var i in obj)
|
|
{
|
|
if (i != mxObjectIdentity.FIELD_NAME && (transients == null ||
|
|
mxUtils.indexOf(transients, i) < 0))
|
|
{
|
|
if (!shallow && typeof(obj[i]) == 'object')
|
|
{
|
|
clone[i] = mxUtils.clone(obj[i]);
|
|
}
|
|
else
|
|
{
|
|
clone[i] = obj[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return clone;
|
|
},
|
|
|
|
/**
|
|
* Function: equalPoints
|
|
*
|
|
* Compares all mxPoints in the given lists.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* a - Array of <mxPoints> to be compared.
|
|
* b - Array of <mxPoints> to be compared.
|
|
*/
|
|
equalPoints: function(a, b)
|
|
{
|
|
if ((a == null && b != null) || (a != null && b == null) ||
|
|
(a != null && b != null && a.length != b.length))
|
|
{
|
|
return false;
|
|
}
|
|
else if (a != null && b != null)
|
|
{
|
|
for (var i = 0; i < a.length; i++)
|
|
{
|
|
if ((a[i] != null && b[i] == null) ||
|
|
(a[i] == null && b[i] != null) ||
|
|
(a[i] != null && b[i] != null &&
|
|
(a[i].x != b[i].x || a[i].y != b[i].y)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Function: equalEntries
|
|
*
|
|
* Returns true if all properties of the given objects are equal. Values
|
|
* with NaN are equal to NaN and unequal to any other value.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* a - First object to be compared.
|
|
* b - Second object to be compared.
|
|
*/
|
|
equalEntries: function(a, b)
|
|
{
|
|
// Counts keys in b to check if all values have been compared
|
|
var count = 0;
|
|
|
|
if ((a == null && b != null) || (a != null && b == null) ||
|
|
(a != null && b != null && a.length != b.length))
|
|
{
|
|
return false;
|
|
}
|
|
else if (a != null && b != null)
|
|
{
|
|
for (var key in b)
|
|
{
|
|
count++;
|
|
}
|
|
|
|
for (var key in a)
|
|
{
|
|
count--
|
|
|
|
if ((!mxUtils.isNaN(a[key]) || !mxUtils.isNaN(b[key])) && a[key] != b[key])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count == 0;
|
|
},
|
|
|
|
/**
|
|
* Function: removeDuplicates
|
|
*
|
|
* Removes all duplicates from the given array.
|
|
*/
|
|
removeDuplicates: function(arr)
|
|
{
|
|
var dict = new mxDictionary();
|
|
var result = [];
|
|
|
|
for (var i = 0; i < arr.length; i++)
|
|
{
|
|
if (!dict.get(arr[i]))
|
|
{
|
|
result.push(arr[i]);
|
|
dict.put(arr[i], true);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Function: isNaN
|
|
*
|
|
* Returns true if the given value is of type number and isNaN returns true.
|
|
*/
|
|
isNaN: function(value)
|
|
{
|
|
return typeof(value) == 'number' && isNaN(value);
|
|
},
|
|
|
|
/**
|
|
* Function: extend
|
|
*
|
|
* Assigns a copy of the superclass prototype to the subclass prototype.
|
|
* Note that this does not call the constructor of the superclass at this
|
|
* point, the superclass constructor should be called explicitely in the
|
|
* subclass constructor. Below is an example.
|
|
*
|
|
* (code)
|
|
* MyGraph = function(container, model, renderHint, stylesheet)
|
|
* {
|
|
* mxGraph.call(this, container, model, renderHint, stylesheet);
|
|
* }
|
|
*
|
|
* mxUtils.extend(MyGraph, mxGraph);
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* ctor - Constructor of the subclass.
|
|
* superCtor - Constructor of the superclass.
|
|
*/
|
|
extend: function(ctor, superCtor)
|
|
{
|
|
var f = function() {};
|
|
f.prototype = superCtor.prototype;
|
|
|
|
ctor.prototype = new f();
|
|
ctor.prototype.constructor = ctor;
|
|
},
|
|
|
|
/**
|
|
* Function: toString
|
|
*
|
|
* Returns a textual representation of the specified object.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* obj - Object to return the string representation for.
|
|
*/
|
|
toString: function(obj)
|
|
{
|
|
var output = '';
|
|
|
|
for (var i in obj)
|
|
{
|
|
try
|
|
{
|
|
if (obj[i] == null)
|
|
{
|
|
output += i + ' = [null]\n';
|
|
}
|
|
else if (typeof(obj[i]) == 'function')
|
|
{
|
|
output += i + ' => [Function]\n';
|
|
}
|
|
else if (typeof(obj[i]) == 'object')
|
|
{
|
|
var ctor = mxUtils.getFunctionName(obj[i].constructor);
|
|
output += i + ' => [' + ctor + ']\n';
|
|
}
|
|
else
|
|
{
|
|
output += i + ' = ' + obj[i] + '\n';
|
|
}
|
|
}
|
|
catch (e)
|
|
{
|
|
output += i + '=' + e.message;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
},
|
|
|
|
/**
|
|
* Function: toRadians
|
|
*
|
|
* Converts the given degree to radians.
|
|
*/
|
|
toRadians: function(deg)
|
|
{
|
|
return Math.PI * deg / 180;
|
|
},
|
|
|
|
/**
|
|
* Function: toDegree
|
|
*
|
|
* Converts the given radians to degree.
|
|
*/
|
|
toDegree: function(rad)
|
|
{
|
|
return rad * 180 / Math.PI;
|
|
},
|
|
|
|
/**
|
|
* Function: arcToCurves
|
|
*
|
|
* Converts the given arc to a series of curves.
|
|
*/
|
|
arcToCurves: function(x0, y0, r1, r2, angle, largeArcFlag, sweepFlag, x, y)
|
|
{
|
|
x -= x0;
|
|
y -= y0;
|
|
|
|
if (r1 === 0 || r2 === 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var fS = sweepFlag;
|
|
var psai = angle;
|
|
r1 = Math.abs(r1);
|
|
r2 = Math.abs(r2);
|
|
var ctx = -x / 2;
|
|
var cty = -y / 2;
|
|
var cpsi = Math.cos(psai * Math.PI / 180);
|
|
var spsi = Math.sin(psai * Math.PI / 180);
|
|
var rxd = cpsi * ctx + spsi * cty;
|
|
var ryd = -1 * spsi * ctx + cpsi * cty;
|
|
var rxdd = rxd * rxd;
|
|
var rydd = ryd * ryd;
|
|
var r1x = r1 * r1;
|
|
var r2y = r2 * r2;
|
|
var lamda = rxdd / r1x + rydd / r2y;
|
|
var sds;
|
|
|
|
if (lamda > 1)
|
|
{
|
|
r1 = Math.sqrt(lamda) * r1;
|
|
r2 = Math.sqrt(lamda) * r2;
|
|
sds = 0;
|
|
}
|
|
else
|
|
{
|
|
var seif = 1;
|
|
|
|
if (largeArcFlag === fS)
|
|
{
|
|
seif = -1;
|
|
}
|
|
|
|
sds = seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd));
|
|
}
|
|
|
|
var txd = sds * r1 * ryd / r2;
|
|
var tyd = -1 * sds * r2 * rxd / r1;
|
|
var tx = cpsi * txd - spsi * tyd + x / 2;
|
|
var ty = spsi * txd + cpsi * tyd + y / 2;
|
|
var rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1);
|
|
var s1 = (rad >= 0) ? rad : 2 * Math.PI + rad;
|
|
rad = Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) - Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1);
|
|
var dr = (rad >= 0) ? rad : 2 * Math.PI + rad;
|
|
|
|
if (fS == 0 && dr > 0)
|
|
{
|
|
dr -= 2 * Math.PI;
|
|
}
|
|
else if (fS != 0 && dr < 0)
|
|
{
|
|
dr += 2 * Math.PI;
|
|
}
|
|
|
|
var sse = dr * 2 / Math.PI;
|
|
var seg = Math.ceil(sse < 0 ? -1 * sse : sse);
|
|
var segr = dr / seg;
|
|
var t = 8/3 * Math.sin(segr / 4) * Math.sin(segr / 4) / Math.sin(segr / 2);
|
|
var cpsir1 = cpsi * r1;
|
|
var cpsir2 = cpsi * r2;
|
|
var spsir1 = spsi * r1;
|
|
var spsir2 = spsi * r2;
|
|
var mc = Math.cos(s1);
|
|
var ms = Math.sin(s1);
|
|
var x2 = -t * (cpsir1 * ms + spsir2 * mc);
|
|
var y2 = -t * (spsir1 * ms - cpsir2 * mc);
|
|
var x3 = 0;
|
|
var y3 = 0;
|
|
|
|
var result = [];
|
|
|
|
for (var n = 0; n < seg; ++n)
|
|
{
|
|
s1 += segr;
|
|
mc = Math.cos(s1);
|
|
ms = Math.sin(s1);
|
|
|
|
x3 = cpsir1 * mc - spsir2 * ms + tx;
|
|
y3 = spsir1 * mc + cpsir2 * ms + ty;
|
|
var dx = -t * (cpsir1 * ms + spsir2 * mc);
|
|
var dy = -t * (spsir1 * ms - cpsir2 * mc);
|
|
|
|
// CurveTo updates x0, y0 so need to restore it
|
|
var index = n * 6;
|
|
result[index] = Number(x2 + x0);
|
|
result[index + 1] = Number(y2 + y0);
|
|
result[index + 2] = Number(x3 - dx + x0);
|
|
result[index + 3] = Number(y3 - dy + y0);
|
|
result[index + 4] = Number(x3 + x0);
|
|
result[index + 5] = Number(y3 + y0);
|
|
|
|
x2 = x3 + dx;
|
|
y2 = y3 + dy;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Function: getBoundingBox
|
|
*
|
|
* Returns the bounding box for the rotated rectangle.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* rect - <mxRectangle> to be rotated.
|
|
* angle - Number that represents the angle (in degrees).
|
|
* cx - Optional <mxPoint> that represents the rotation center. If no
|
|
* rotation center is given then the center of rect is used.
|
|
*/
|
|
getBoundingBox: function(rect, rotation, cx)
|
|
{
|
|
var result = null;
|
|
|
|
if (rect != null && rotation != null && rotation != 0)
|
|
{
|
|
var rad = mxUtils.toRadians(rotation);
|
|
var cos = Math.cos(rad);
|
|
var sin = Math.sin(rad);
|
|
|
|
cx = (cx != null) ? cx : new mxPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
|
|
var p1 = new mxPoint(rect.x, rect.y);
|
|
var p2 = new mxPoint(rect.x + rect.width, rect.y);
|
|
var p3 = new mxPoint(p2.x, rect.y + rect.height);
|
|
var p4 = new mxPoint(rect.x, p3.y);
|
|
|
|
p1 = mxUtils.getRotatedPoint(p1, cos, sin, cx);
|
|
p2 = mxUtils.getRotatedPoint(p2, cos, sin, cx);
|
|
p3 = mxUtils.getRotatedPoint(p3, cos, sin, cx);
|
|
p4 = mxUtils.getRotatedPoint(p4, cos, sin, cx);
|
|
|
|
result = new mxRectangle(p1.x, p1.y, 0, 0);
|
|
result.add(new mxRectangle(p2.x, p2.y, 0, 0));
|
|
result.add(new mxRectangle(p3.x, p3.y, 0, 0));
|
|
result.add(new mxRectangle(p4.x, p4.y, 0, 0));
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Function: getRotatedPoint
|
|
*
|
|
* Rotates the given point by the given cos and sin.
|
|
*/
|
|
getRotatedPoint: function(pt, cos, sin, c)
|
|
{
|
|
c = (c != null) ? c : new mxPoint();
|
|
var x = pt.x - c.x;
|
|
var y = pt.y - c.y;
|
|
|
|
var x1 = x * 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.5;
|
|
var dy = -0.5;
|
|
|
|
// Horizontal alignment
|
|
if (align == mxConstants.ALIGN_LEFT)
|
|
{
|
|
dx = 0;
|
|
}
|
|
else if (align == mxConstants.ALIGN_RIGHT)
|
|
{
|
|
dx = -1;
|
|
}
|
|
|
|
// Vertical alignment
|
|
if (valign == mxConstants.ALIGN_TOP)
|
|
{
|
|
dy = 0;
|
|
}
|
|
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.
|
|
* fontStyle - Optional font style.
|
|
*/
|
|
getSizeForString: function(text, fontSize, fontFamily, textWidth, fontStyle)
|
|
{
|
|
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';
|
|
|
|
// Sets the font style
|
|
if (fontStyle != null)
|
|
{
|
|
if ((fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
|
|
{
|
|
div.style.fontWeight = 'bold';
|
|
}
|
|
|
|
if ((fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
|
|
{
|
|
div.style.fontStyle = 'italic';
|
|
}
|
|
|
|
var txtDecor = [];
|
|
|
|
if ((fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
txtDecor.push('underline');
|
|
}
|
|
|
|
if ((fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
txtDecor.push('line-through');
|
|
}
|
|
|
|
if (txtDecor.length > 0)
|
|
{
|
|
div.style.textDecoration = txtDecor.join(' ');
|
|
}
|
|
}
|
|
|
|
// 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, ' ');
|
|
|
|
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, ' ');
|
|
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')); //
|
|
div.appendChild(document.createTextNode('\u00a0')); //
|
|
div.appendChild(document.createTextNode('\u00a0')); //
|
|
mxUtils.write(div, message);
|
|
|
|
var w = document.body.clientWidth;
|
|
var h = (document.body.clientHeight || document.documentElement.clientHeight);
|
|
var warn = new mxWindow(mxResources.get(mxUtils.errorResource) ||
|
|
mxUtils.errorResource, div, (w-width)/2, h/4, width, null,
|
|
false, true);
|
|
|
|
if (close)
|
|
{
|
|
mxUtils.br(div);
|
|
|
|
var tmp = document.createElement('p');
|
|
var button = document.createElement('button');
|
|
|
|
if (mxClient.IS_IE)
|
|
{
|
|
button.style.cssText = 'float:right';
|
|
}
|
|
else
|
|
{
|
|
button.setAttribute('style', 'float:right');
|
|
}
|
|
|
|
mxEvent.addListener(button, 'click', function(evt)
|
|
{
|
|
warn.destroy();
|
|
});
|
|
|
|
mxUtils.write(button, mxResources.get(mxUtils.closeResource) ||
|
|
mxUtils.closeResource);
|
|
|
|
tmp.appendChild(button);
|
|
div.appendChild(tmp);
|
|
|
|
mxUtils.br(div);
|
|
|
|
warn.setClosable(true);
|
|
}
|
|
|
|
warn.setVisible(true);
|
|
|
|
return warn;
|
|
},
|
|
|
|
/**
|
|
* Function: makeDraggable
|
|
*
|
|
* Configures the given DOM element to act as a drag source for the
|
|
* specified graph. Returns a a new <mxDragSource>. If
|
|
* <mxDragSource.guideEnabled> is enabled then the x and y arguments must
|
|
* be used in funct to match the preview location.
|
|
*
|
|
* Example:
|
|
*
|
|
* (code)
|
|
* var funct = function(graph, evt, cell, x, y)
|
|
* {
|
|
* if (graph.canImportCell(cell))
|
|
* {
|
|
* var parent = graph.getDefaultParent();
|
|
* var vertex = null;
|
|
*
|
|
* graph.getModel().beginUpdate();
|
|
* try
|
|
* {
|
|
* vertex = graph.insertVertex(parent, null, 'Hello', x, y, 80, 30);
|
|
* }
|
|
* finally
|
|
* {
|
|
* graph.getModel().endUpdate();
|
|
* }
|
|
*
|
|
* graph.setSelectionCell(vertex);
|
|
* }
|
|
* }
|
|
*
|
|
* var img = document.createElement('img');
|
|
* img.setAttribute('src', 'editors/images/rectangle.gif');
|
|
* img.style.position = 'absolute';
|
|
* img.style.left = '0px';
|
|
* img.style.top = '0px';
|
|
* img.style.width = '16px';
|
|
* img.style.height = '16px';
|
|
*
|
|
* var dragImage = img.cloneNode(true);
|
|
* dragImage.style.width = '32px';
|
|
* dragImage.style.height = '32px';
|
|
* mxUtils.makeDraggable(img, graph, funct, dragImage);
|
|
* document.body.appendChild(img);
|
|
* (end)
|
|
*
|
|
* Parameters:
|
|
*
|
|
* element - DOM element to make draggable.
|
|
* graphF - <mxGraph> that acts as the drop target or a function that takes a
|
|
* mouse event and returns the current <mxGraph>.
|
|
* funct - Function to execute on a successful drop.
|
|
* dragElement - Optional DOM node to be used for the drag preview.
|
|
* dx - Optional horizontal offset between the cursor and the drag
|
|
* preview.
|
|
* dy - Optional vertical offset between the cursor and the drag
|
|
* preview.
|
|
* autoscroll - Optional boolean that specifies if autoscroll should be
|
|
* used. Default is mxGraph.autoscroll.
|
|
* scalePreview - Optional boolean that specifies if the preview element
|
|
* should be scaled according to the graph scale. If this is true, then
|
|
* the offsets will also be scaled. Default is false.
|
|
* highlightDropTargets - Optional boolean that specifies if dropTargets
|
|
* should be highlighted. Default is true.
|
|
* getDropTarget - Optional function to return the drop target for a given
|
|
* location (x, y). Default is mxGraph.getCellAt.
|
|
*/
|
|
makeDraggable: function(element, graphF, funct, dragElement, dx, dy, autoscroll,
|
|
scalePreview, highlightDropTargets, getDropTarget)
|
|
{
|
|
var dragSource = new mxDragSource(element, funct);
|
|
dragSource.dragOffset = new mxPoint((dx != null) ? dx : 0,
|
|
(dy != null) ? dy : mxConstants.TOOLTIP_VERTICAL_OFFSET);
|
|
dragSource.autoscroll = autoscroll;
|
|
|
|
// Cannot enable this by default. This needs to be enabled in the caller
|
|
// if the funct argument uses the new x- and y-arguments.
|
|
dragSource.setGuidesEnabled(false);
|
|
|
|
if (highlightDropTargets != null)
|
|
{
|
|
dragSource.highlightDropTargets = highlightDropTargets;
|
|
}
|
|
|
|
// Overrides function to find drop target cell
|
|
if (getDropTarget != null)
|
|
{
|
|
dragSource.getDropTarget = getDropTarget;
|
|
}
|
|
|
|
// Overrides function to get current graph
|
|
dragSource.getGraphForEvent = function(evt)
|
|
{
|
|
return (typeof(graphF) == 'function') ? graphF(evt) : graphF;
|
|
};
|
|
|
|
// Translates switches into dragSource customizations
|
|
if (dragElement != null)
|
|
{
|
|
dragSource.createDragElement = function()
|
|
{
|
|
return dragElement.cloneNode(true);
|
|
};
|
|
|
|
if (scalePreview)
|
|
{
|
|
dragSource.createPreviewElement = function(graph)
|
|
{
|
|
var elt = dragElement.cloneNode(true);
|
|
|
|
var w = parseInt(elt.style.width);
|
|
var h = parseInt(elt.style.height);
|
|
elt.style.width = Math.round(w * graph.view.scale) + 'px';
|
|
elt.style.height = Math.round(h * graph.view.scale) + 'px';
|
|
|
|
return elt;
|
|
};
|
|
}
|
|
}
|
|
|
|
return dragSource;
|
|
}
|
|
|
|
};
|
|
/**
|
|
* 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: FONT_STRIKETHROUGH
|
|
*
|
|
* Constant for strikthrough fonts. Default is 8.
|
|
*/
|
|
FONT_STRIKETHROUGH: 8,
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
//To prevent window zoom on trackpad pinch
|
|
if (evt.ctrlKey)
|
|
{
|
|
evt.preventDefault();
|
|
}
|
|
|
|
var delta = -evt.deltaY;
|
|
|
|
// Handles the event using the given function
|
|
if (Math.abs(evt.deltaX) > 0.5 || Math.abs(evt.deltaY) > 0.5)
|
|
{
|
|
funct(evt, (evt.deltaY == 0) ? -evt.deltaX > 0 : -evt.deltaY > 0);
|
|
}
|
|
};
|
|
|
|
target = target != null ? target : window;
|
|
|
|
if (mxClient.IS_SF && !mxClient.IS_TOUCH)
|
|
{
|
|
var scale = 1;
|
|
|
|
mxEvent.addListener(target, 'gesturestart', function(evt)
|
|
{
|
|
mxEvent.consume(evt);
|
|
scale = 1;
|
|
});
|
|
|
|
mxEvent.addListener(target, 'gesturechange', function(evt)
|
|
{
|
|
mxEvent.consume(evt);
|
|
var diff = scale - evt.scale;
|
|
|
|
if (Math.abs(diff) > 0.2)
|
|
{
|
|
funct(evt, diff < 0, true);
|
|
scale = evt.scale;
|
|
}
|
|
});
|
|
|
|
mxEvent.addListener(target, 'gestureend', function(evt)
|
|
{
|
|
mxEvent.consume(evt);
|
|
});
|
|
}
|
|
|
|
mxEvent.addListener(target, 'wheel', 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-2020, JGraph Ltd
|
|
* Copyright (c) 2006-2020, draw.io AG
|
|
*/
|
|
/**
|
|
* 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", "
");
|
|
* (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 != null) ? this.request.status : null;
|
|
};
|
|
|
|
/**
|
|
* 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.onreadystatechange = 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 == null ||
|
|
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 != null && 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: 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: getAlternateText
|
|
*
|
|
* Returns the alternate text string for the given foreignObject.
|
|
*/
|
|
mxSvgCanvas2D.prototype.getAlternateText = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
|
|
{
|
|
return (str != null) ? this.foAltText : null;
|
|
};
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
var text = this.getAlternateText(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation);
|
|
var s = this.state;
|
|
|
|
if (text != null && s.fontSize > 0)
|
|
{
|
|
var dy = (valign == mxConstants.ALIGN_TOP) ? 1 :
|
|
(valign == mxConstants.ALIGN_BOTTOM) ? 0 : 0.3;
|
|
var anchor = (align == mxConstants.ALIGN_RIGHT) ? 'end' :
|
|
(align == mxConstants.ALIGN_LEFT) ? 'start' :
|
|
'middle';
|
|
|
|
var alt = this.createElement('text');
|
|
alt.setAttribute('x', Math.round(x + s.dx));
|
|
alt.setAttribute('y', Math.round(y + s.dy + dy * s.fontSize));
|
|
alt.setAttribute('fill', s.fontColor || 'black');
|
|
alt.setAttribute('font-family', s.fontFamily);
|
|
alt.setAttribute('font-size', Math.round(s.fontSize) + 'px');
|
|
|
|
// Text-anchor start is default in SVG
|
|
if (anchor != 'start')
|
|
{
|
|
alt.setAttribute('text-anchor', anchor);
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
var txtDecor = [];
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
txtDecor.push('underline');
|
|
}
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
txtDecor.push('line-through');
|
|
}
|
|
|
|
if (txtDecor.length > 0)
|
|
{
|
|
alt.setAttribute('text-decoration', txtDecor.join(' '));
|
|
}
|
|
|
|
mxUtils.write(alt, text);
|
|
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* 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(/&/g, '&amp;').
|
|
replace(/</g, '&lt;').replace(/>/g, '&gt;').
|
|
replace(/</g, '&lt;').replace(/>/g, '&gt;').
|
|
replace(/</g, '<').replace(/>/g, '>');
|
|
val = ta.value.replace(/&/g, '&').replace(/&lt;/g, '<').
|
|
replace(/&gt;/g, '>').replace(/&amp;/g, '&').
|
|
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)
|
|
{
|
|
var val = str;
|
|
|
|
if (!mxUtils.isNode(val))
|
|
{
|
|
val = '<div><div>' + this.convertHtml(val) + '</div></div>';
|
|
}
|
|
|
|
// IE uses this code for export as it cannot render foreignObjects
|
|
if (!mxClient.IS_IE && !mxClient.IS_IE11 && document.createElementNS)
|
|
{
|
|
var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
|
|
|
if (mxUtils.isNode(val))
|
|
{
|
|
var div2 = document.createElement('div');
|
|
var div3 = div2.cloneNode(false);
|
|
|
|
// Creates a copy for export
|
|
if (this.root.ownerDocument != document)
|
|
{
|
|
div2.appendChild(val.cloneNode(true));
|
|
}
|
|
else
|
|
{
|
|
div2.appendChild(val);
|
|
}
|
|
|
|
div3.appendChild(div2);
|
|
div.appendChild(div3);
|
|
}
|
|
else
|
|
{
|
|
div.innerHTML = val;
|
|
}
|
|
|
|
return div;
|
|
}
|
|
else
|
|
{
|
|
if (mxUtils.isNode(val))
|
|
{
|
|
val = '<div><div>' + mxUtils.getXml(val) + '</div></div>';
|
|
}
|
|
|
|
val = '<div xmlns="http://www.w3.org/1999/xhtml">' + val + '</div>';
|
|
|
|
// NOTE: FF 3.6 crashes if content CSS contains "height:100%"
|
|
return mxUtils.parseXml(val).documentElement;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
this.updateTextNodes(x, y, w, h, align, valign, wrap, overflow, clip, rotation, node.firstChild);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: addForeignObject
|
|
*
|
|
* Creates a foreignObject for the given string and adds it to the given root.
|
|
*/
|
|
mxSvgCanvas2D.prototype.addForeignObject = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir, div, root)
|
|
{
|
|
var group = this.createElement('g');
|
|
var fo = this.createElement('foreignObject');
|
|
|
|
// Workarounds for print clipping and static position in Safari
|
|
fo.setAttribute('style', 'overflow: visible; text-align: left;');
|
|
fo.setAttribute('pointer-events', 'none');
|
|
|
|
// Import needed for older versions of IE
|
|
if (div.ownerDocument != document)
|
|
{
|
|
div = mxUtils.importNodeImplementation(fo.ownerDocument, div, true);
|
|
}
|
|
|
|
fo.appendChild(div);
|
|
group.appendChild(fo);
|
|
|
|
this.updateTextNodes(x, y, w, h, align, valign, wrap, overflow, clip, rotation, group);
|
|
|
|
// Alternate content if foreignObject not supported
|
|
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);
|
|
}
|
|
}
|
|
|
|
root.appendChild(group);
|
|
};
|
|
|
|
/**
|
|
* Updates existing DOM nodes for text rendering.
|
|
*/
|
|
mxSvgCanvas2D.prototype.updateTextNodes = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, g)
|
|
{
|
|
var s = this.state.scale;
|
|
|
|
mxSvgCanvas2D.createCss(w + 2, h, align, valign, wrap, overflow, clip,
|
|
(this.state.fontBackgroundColor != null) ? this.state.fontBackgroundColor : null,
|
|
(this.state.fontBorderColor != null) ? this.state.fontBorderColor : null,
|
|
'display: flex; align-items: unsafe ' +
|
|
((valign == mxConstants.ALIGN_TOP) ? 'flex-start' :
|
|
((valign == mxConstants.ALIGN_BOTTOM) ? 'flex-end' : 'center')) + '; ' +
|
|
'justify-content: unsafe ' + ((align == mxConstants.ALIGN_LEFT) ? 'flex-start' :
|
|
((align == mxConstants.ALIGN_RIGHT) ? 'flex-end' : 'center')) + '; ',
|
|
this.getTextCss(), s, mxUtils.bind(this, function(dx, dy, flex, item, block)
|
|
{
|
|
x += this.state.dx;
|
|
y += this.state.dy;
|
|
|
|
var fo = g.firstChild;
|
|
var div = fo.firstChild;
|
|
var box = div.firstChild;
|
|
var text = box.firstChild;
|
|
var r = ((this.rotateHtml) ? this.state.rotation : 0) + ((rotation != null) ? rotation : 0);
|
|
var t = ((this.foOffset != 0) ? 'translate(' + this.foOffset + ' ' + this.foOffset + ')' : '') +
|
|
((s != 1) ? 'scale(' + s + ')' : '');
|
|
|
|
text.setAttribute('style', block);
|
|
box.setAttribute('style', item);
|
|
|
|
// Workaround for clipping in Webkit with scrolling and zoom
|
|
fo.setAttribute('width', Math.ceil(1 / Math.min(1, s) * 100) + '%');
|
|
fo.setAttribute('height', Math.ceil(1 / Math.min(1, s) * 100) + '%');
|
|
var yp = Math.round(y + dy);
|
|
|
|
// Allows for negative values which are causing problems with
|
|
// transformed content where the top edge of the foreignObject
|
|
// limits the text box being moved further up in the diagram.
|
|
// KNOWN: Possible clipping problems with zoom and scrolling
|
|
// but this is normally not used with scrollbars as the
|
|
// coordinates are always positive with scrollbars.
|
|
// Margin-top is ignored in Safari and no negative values allowed
|
|
// for padding.
|
|
if (yp < 0)
|
|
{
|
|
fo.setAttribute('y', yp);
|
|
}
|
|
else
|
|
{
|
|
fo.removeAttribute('y');
|
|
flex += 'padding-top: ' + yp + 'px; ';
|
|
}
|
|
|
|
div.setAttribute('style', flex + 'margin-left: ' + Math.round(x + dx) + 'px;');
|
|
t += ((r != 0) ? ('rotate(' + r + ' ' + x + ' ' + y + ')') : '');
|
|
|
|
// Output allows for reflow but Safari cannot use absolute position,
|
|
// transforms or opacity. https://bugs.webkit.org/show_bug.cgi?id=23113
|
|
if (t != '')
|
|
{
|
|
g.setAttribute('transform', t);
|
|
}
|
|
else
|
|
{
|
|
g.removeAttribute('transform');
|
|
}
|
|
|
|
if (this.state.alpha != 1)
|
|
{
|
|
g.setAttribute('opacity', this.state.alpha);
|
|
}
|
|
else
|
|
{
|
|
g.removeAttribute('opacity');
|
|
}
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* Updates existing DOM nodes for text rendering.
|
|
*/
|
|
mxSvgCanvas2D.createCss = function(w, h, align, valign, wrap, overflow, clip, bg, border, flex, block, s, callback)
|
|
{
|
|
var item = 'box-sizing: border-box; font-size: 0; text-align: ' + ((align == mxConstants.ALIGN_LEFT) ? 'left' :
|
|
((align == mxConstants.ALIGN_RIGHT) ? 'right' : 'center')) + '; ';
|
|
var pt = mxUtils.getAlignmentAsPoint(align, valign);
|
|
var ofl = 'overflow: hidden; ';
|
|
var fw = 'width: 1px; ';
|
|
var fh = 'height: 1px; ';
|
|
var dx = pt.x * w;
|
|
var dy = pt.y * h;
|
|
|
|
if (clip)
|
|
{
|
|
fw = 'width: ' + Math.round(w) + 'px; ';
|
|
item += 'max-height: ' + Math.round(h) + 'px; ';
|
|
dy = 0;
|
|
}
|
|
else if (overflow == 'fill')
|
|
{
|
|
fw = 'width: ' + Math.round(w) + 'px; ';
|
|
fh = 'height: ' + Math.round(h) + 'px; ';
|
|
block += 'width: 100%; height: 100%; ';
|
|
item += fw + fh;
|
|
}
|
|
else if (overflow == 'width')
|
|
{
|
|
fw = 'width: ' + Math.round(w) + 'px; ';
|
|
block += 'width: 100%; ';
|
|
item += fw;
|
|
dy = 0;
|
|
|
|
if (h > 0)
|
|
{
|
|
item += 'max-height: ' + Math.round(h) + 'px; ';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ofl = '';
|
|
dy = 0;
|
|
}
|
|
|
|
var bgc = '';
|
|
|
|
if (bg != null)
|
|
{
|
|
bgc += 'background-color: ' + bg + '; ';
|
|
}
|
|
|
|
if (border != null)
|
|
{
|
|
bgc += 'border: 1px solid ' + border + '; ';
|
|
}
|
|
|
|
if (ofl == '' || clip)
|
|
{
|
|
block += bgc;
|
|
}
|
|
else
|
|
{
|
|
item += bgc;
|
|
}
|
|
|
|
if (wrap && w > 0)
|
|
{
|
|
block += 'white-space: normal; word-wrap: ' + mxConstants.WORD_WRAP + '; ';
|
|
fw = 'width: ' + Math.round(w) + 'px; ';
|
|
|
|
if (ofl != '' && overflow != 'fill')
|
|
{
|
|
dy = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
block += 'white-space: nowrap; ';
|
|
|
|
if (ofl == '')
|
|
{
|
|
dx = 0;
|
|
}
|
|
}
|
|
|
|
callback(dx, dy, flex + fw + fh, item + ofl, block, ofl);
|
|
};
|
|
|
|
/**
|
|
* Function: getTextCss
|
|
*
|
|
* Private helper function to create SVG elements
|
|
*/
|
|
mxSvgCanvas2D.prototype.getTextCss = function()
|
|
{
|
|
var s = this.state;
|
|
var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' :
|
|
(mxConstants.LINE_HEIGHT * this.lineHeightCorrection);
|
|
var css = 'display: inline-block; font-size: ' + s.fontSize + 'px; ' +
|
|
'font-family: ' + s.fontFamily + '; color: ' + s.fontColor + '; line-height: ' + lh +
|
|
'; pointer-events: ' + ((this.pointerEvents) ? this.pointerEventsValue : 'none') + '; ';
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
|
|
{
|
|
css += 'font-weight: bold; ';
|
|
}
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
|
|
{
|
|
css += 'font-style: italic; ';
|
|
}
|
|
|
|
var deco = [];
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
deco.push('underline');
|
|
}
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
deco.push('line-through');
|
|
}
|
|
|
|
if (deco.length > 0)
|
|
{
|
|
css += 'text-decoration: ' + deco.join(' ') + '; ';
|
|
}
|
|
|
|
return css;
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
if (this.foEnabled && format == 'html')
|
|
{
|
|
var div = this.createDiv(str);
|
|
|
|
// Ignores invalid XHTML labels
|
|
if (div != null)
|
|
{
|
|
if (dir != null)
|
|
{
|
|
div.setAttribute('dir', dir);
|
|
}
|
|
|
|
this.addForeignObject(x, y, w, h, str, align, valign, wrap,
|
|
format, overflow, clip, rotation, dir, div, this.root);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.plainText(x + this.state.dx, y + this.state.dy, 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;
|
|
}
|
|
}
|
|
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 + 1;
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
var txtDecor = [];
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
txtDecor.push('underline');
|
|
}
|
|
|
|
if ((s.fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
txtDecor.push('line-through');
|
|
}
|
|
|
|
if (txtDecor.length > 0)
|
|
{
|
|
node.setAttribute('text-decoration', txtDecor.join(' '));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* Variable: tolerance
|
|
*
|
|
* Default tolerance in px if grid is disabled. Default is 2.
|
|
*/
|
|
mxGuide.prototype.tolerance = 2;
|
|
|
|
/**
|
|
* 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(gridEnabled)
|
|
{
|
|
return (gridEnabled && this.graph.gridEnabled) ? this.graph.gridSize / 2 : this.tolerance;
|
|
};
|
|
|
|
/**
|
|
* 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 tt = this.getGuideTolerance(gridEnabled) * scale;
|
|
var b = bounds.clone();
|
|
b.x += delta.x;
|
|
b.y += delta.y;
|
|
var overrideX = false;
|
|
var stateX = null;
|
|
var valueX = null;
|
|
var overrideY = false;
|
|
var stateY = null;
|
|
var valueY = null;
|
|
var ttX = tt;
|
|
var ttY = tt;
|
|
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, centerAlign)
|
|
{
|
|
var override = false;
|
|
|
|
if (centerAlign && Math.abs(x - center) < ttX)
|
|
{
|
|
delta.x = x - bounds.getCenterX();
|
|
ttX = Math.abs(x - center);
|
|
override = true;
|
|
}
|
|
else if (!centerAlign)
|
|
{
|
|
if (Math.abs(x - left) < ttX)
|
|
{
|
|
delta.x = x - bounds.x;
|
|
ttX = Math.abs(x - left);
|
|
override = true;
|
|
}
|
|
else if (Math.abs(x - right) < ttX)
|
|
{
|
|
delta.x = x - bounds.x - bounds.width;
|
|
ttX = Math.abs(x - right);
|
|
override = true;
|
|
}
|
|
}
|
|
|
|
if (override)
|
|
{
|
|
stateX = state;
|
|
valueX = x;
|
|
|
|
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, centerAlign)
|
|
{
|
|
var override = false;
|
|
|
|
if (centerAlign && Math.abs(y - middle) < ttY)
|
|
{
|
|
delta.y = y - bounds.getCenterY();
|
|
ttY = Math.abs(y - middle);
|
|
override = true;
|
|
}
|
|
else if (!centerAlign)
|
|
{
|
|
if (Math.abs(y - top) < ttY)
|
|
{
|
|
delta.y = y - bounds.y;
|
|
ttY = Math.abs(y - top);
|
|
override = true;
|
|
}
|
|
else if (Math.abs(y - bottom) < ttY)
|
|
{
|
|
delta.y = y - bounds.y - bounds.height;
|
|
ttY = Math.abs(y - bottom);
|
|
override = true;
|
|
}
|
|
}
|
|
|
|
if (override)
|
|
{
|
|
stateY = state;
|
|
valueY = y;
|
|
|
|
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, true);
|
|
snapX.call(this, state.x, state, false);
|
|
snapX.call(this, state.x + state.width, state, false);
|
|
|
|
// Aligns left and right of shape to center of page
|
|
if (state.cell == null)
|
|
{
|
|
snapX.call(this, state.getCenterX(), state, false);
|
|
}
|
|
}
|
|
|
|
// Align y
|
|
if (this.vertical)
|
|
{
|
|
snapY.call(this, state.getCenterY(), state, true);
|
|
snapY.call(this, state.y, state, false);
|
|
snapY.call(this, state.y + state.height, state, false);
|
|
|
|
// Aligns left and right of shape to center of page
|
|
if (state.cell == null)
|
|
{
|
|
snapY.call(this, state.getCenterY(), state, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Moves cells to the raster if not aligned
|
|
this.graph.snapDelta(delta, bounds, !gridEnabled, overrideX, overrideY);
|
|
delta = this.getDelta(bounds, stateX, delta.x, stateY, delta.y)
|
|
|
|
// 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 + delta.y - this.graph.panDy, stateX.y);
|
|
maxY = Math.max(bounds.y + bounds.height + delta.y - 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 + delta.x - this.graph.panDx, stateY.x);
|
|
maxX = Math.max(bounds.x + bounds.width + delta.x - 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();
|
|
}
|
|
}
|
|
|
|
return delta;
|
|
};
|
|
|
|
/**
|
|
* Function: getDelta
|
|
*
|
|
* Rounds to pixels for virtual states (eg. page guides)
|
|
*/
|
|
mxGuide.prototype.getDelta = function(bounds, stateX, dx, stateY, dy)
|
|
{
|
|
var s = this.graph.view.scale;
|
|
|
|
if (this.rounded || (stateX != null && stateX.cell == null))
|
|
{
|
|
dx = Math.round((bounds.x + dx) / s) * s - bounds.x;
|
|
}
|
|
|
|
if (this.rounded || (stateY != null && stateY.cell == null))
|
|
{
|
|
dy = Math.round((bounds.y + dy) / s) * s - bounds.y;
|
|
}
|
|
|
|
return new mxPoint(dx, dy);
|
|
};
|
|
|
|
/**
|
|
* Function: getGuideColor
|
|
*
|
|
* Returns the color for the given state.
|
|
*/
|
|
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;
|
|
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)
|
|
{
|
|
var stack = canvas.states.slice();
|
|
|
|
// 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);
|
|
|
|
// Restores stack for unequal count of save/restore calls
|
|
if (canvas.states.length != stack.length)
|
|
{
|
|
canvas.states = stack;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
if (update)
|
|
{
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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))
|
|
{
|
|
if (mxClient.IS_SVG)
|
|
{
|
|
this.redrawHtmlShapeWithCss3();
|
|
}
|
|
else
|
|
{
|
|
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)
|
|
{
|
|
// Specifies if events should be handled
|
|
canvas.pointerEvents = this.pointerEvents;
|
|
|
|
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: getContentNode
|
|
*
|
|
* Returns the node that contains the rendered input.
|
|
*/
|
|
mxText.prototype.getContentNode = function()
|
|
{
|
|
var result = this.node;
|
|
|
|
if (result != null)
|
|
{
|
|
// Rendered with no foreignObject
|
|
if (result.ownerSVGElement == null)
|
|
{
|
|
result = this.node.firstChild.firstChild;
|
|
}
|
|
else
|
|
{
|
|
// Innermost DIV that contains the actual content
|
|
result = result.firstChild.firstChild.firstChild.firstChild.firstChild;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* 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')
|
|
{
|
|
// Uses second inner DIV for font metrics
|
|
node = node.firstChild.firstChild.firstChild.firstChild;
|
|
oh = node.offsetHeight * this.scale;
|
|
|
|
if (this.overflow == 'width')
|
|
{
|
|
ow = this.boundingBox.width;
|
|
}
|
|
else
|
|
{
|
|
ow = node.offsetWidth * 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: getHtmlValue
|
|
*
|
|
* Private helper function to create SVG elements
|
|
*/
|
|
mxText.prototype.getHtmlValue = function()
|
|
{
|
|
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;
|
|
|
|
return val;
|
|
};
|
|
|
|
/**
|
|
* Function: getTextCss
|
|
*
|
|
* Private helper function to create SVG elements
|
|
*/
|
|
mxText.prototype.getTextCss = function()
|
|
{
|
|
var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (this.size * mxConstants.LINE_HEIGHT) + 'px' :
|
|
mxConstants.LINE_HEIGHT;
|
|
|
|
var css = 'display: inline-block; font-size: ' + this.size + 'px; ' +
|
|
'font-family: ' + this.family + '; color: ' + this.color + '; line-height: ' + lh +
|
|
'; pointer-events: ' + ((this.pointerEvents) ? 'all' : 'none') + '; ';
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
|
|
{
|
|
css += 'font-weight: bold; ';
|
|
}
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
|
|
{
|
|
css += 'font-style: italic; ';
|
|
}
|
|
|
|
var deco = [];
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
deco.push('underline');
|
|
}
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
deco.push('line-through');
|
|
}
|
|
|
|
if (deco.length > 0)
|
|
{
|
|
css += 'text-decoration: ' + deco.join(' ') + '; ';
|
|
}
|
|
|
|
return css;
|
|
};
|
|
|
|
/**
|
|
* Function: redrawHtmlShape
|
|
*
|
|
* Updates the HTML node(s) to reflect the latest bounds and scale.
|
|
*/
|
|
mxText.prototype.redrawHtmlShape = function()
|
|
{
|
|
if (mxClient.IS_SVG)
|
|
{
|
|
this.redrawHtmlShapeWithCss3();
|
|
}
|
|
else
|
|
{
|
|
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: redrawHtmlShapeWithCss3
|
|
*
|
|
* Updates the HTML node(s) to reflect the latest bounds and scale.
|
|
*/
|
|
mxText.prototype.redrawHtmlShapeWithCss3 = function()
|
|
{
|
|
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 flex = 'position: absolute; left: ' + Math.round(this.bounds.x) + 'px; ' +
|
|
'top: ' + Math.round(this.bounds.y) + 'px; pointer-events: none; ';
|
|
var block = this.getTextCss();
|
|
|
|
mxSvgCanvas2D.createCss(w + 2, h, this.align, this.valign, this.wrap, this.overflow, this.clipped,
|
|
(this.background != null) ? mxUtils.htmlEntities(this.background) : null,
|
|
(this.border != null) ? mxUtils.htmlEntities(this.border) : null,
|
|
flex, block, this.scale, mxUtils.bind(this, function(dx, dy, flex, item, block, ofl)
|
|
{
|
|
var r = this.getTextRotation();
|
|
var tr = ((this.scale != 1) ? 'scale(' + this.scale + ') ' : '') +
|
|
((r != 0) ? 'rotate(' + r + 'deg) ' : '') +
|
|
((this.margin.x != 0 || this.margin.y != 0) ?
|
|
'translate(' + (this.margin.x * 100) + '%,' +
|
|
(this.margin.y * 100) + '%)' : '');
|
|
|
|
if (tr != '')
|
|
{
|
|
tr = 'transform-origin: 0 0; transform: ' + tr + '; ';
|
|
}
|
|
|
|
if (ofl == '')
|
|
{
|
|
flex += item;
|
|
item = 'display:inline-block; min-width: 100%; ' + tr;
|
|
}
|
|
else
|
|
{
|
|
item += tr;
|
|
}
|
|
|
|
if (this.opacity < 100)
|
|
{
|
|
block += 'opacity: ' + (this.opacity / 100) + '; ';
|
|
}
|
|
|
|
this.node.setAttribute('style', flex);
|
|
|
|
var html = (mxUtils.isNode(this.value)) ? this.value.outerHTML : this.getHtmlValue();
|
|
|
|
if (this.node.firstChild == null)
|
|
{
|
|
this.node.innerHTML = '<div><div>' + html +'</div></div>';
|
|
}
|
|
|
|
this.node.firstChild.firstChild.setAttribute('style', block);
|
|
this.node.firstChild.setAttribute('style', item);
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* 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: updateInnerHtml
|
|
*
|
|
* 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> </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 = '';
|
|
}
|
|
|
|
var txtDecor = [];
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
txtDecor.push('underline');
|
|
}
|
|
|
|
if ((this.fontStyle & mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
txtDecor.push('line-through');
|
|
}
|
|
|
|
style.textDecoration = txtDecor.join(' ');
|
|
|
|
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;
|
|
this.indicator.redraw();
|
|
}
|
|
|
|
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: resizeCell
|
|
*
|
|
* Notified when a cell is being resized in a parent that has automatic
|
|
* layout to update the other cells in the layout.
|
|
*
|
|
* Empty implementation.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cell - <mxCell> which has been moved.
|
|
* bounds - <mxRectangle> that represents the new cell bounds.
|
|
*/
|
|
mxGraphLayout.prototype.resizeCell = function(cell, bounds) { };
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
return this.graph.getCurrentCellStyle(cell)[key]
|
|
};
|
|
|
|
/**
|
|
* 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 but ignores
|
|
// edge labels where width and height is set to 0
|
|
if (bbox != null && bbox.width > 0 && bbox.height > 0 &&
|
|
!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.STYLE_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;
|
|
|
|
/**
|
|
* Variable: unscaledHeight
|
|
*
|
|
* Holds the unscaled height of the state.
|
|
*/
|
|
mxCellState.prototype.unscaledHeight = 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;
|
|
this.unscaledHeight = state.unscaledHeight;
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
clone.unscaledHeight = this.unscaledHeight;
|
|
|
|
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)
|
|
{
|
|
var css = mxUtils.getCurrentStyle(root);
|
|
|
|
if (css != null)
|
|
{
|
|
absoluteRoot = css.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 txtDecor = [];
|
|
|
|
if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
|
|
mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
|
|
{
|
|
txtDecor.push('underline');
|
|
}
|
|
|
|
if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
|
|
mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
|
|
{
|
|
txtDecor.push('line-through');
|
|
}
|
|
|
|
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 = txtDecor.join(' ');
|
|
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, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR);
|
|
this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR);
|
|
this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR);
|
|
this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR);
|
|
this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: checkPlaceholderStyles
|
|
*
|
|
* Checks if the style of the given <mxCellState> contains 'inherit',
|
|
* 'indicated' or 'swimlane' for colors that support those keywords.
|
|
*/
|
|
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, mxConstants.STYLE_FONTCOLOR];
|
|
|
|
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 shape = (key == mxConstants.STYLE_FONTCOLOR) ?
|
|
state.text : state.shape;
|
|
|
|
if (shape != null)
|
|
{
|
|
var graph = state.view.graph;
|
|
var value = shape[field];
|
|
var referenced = null;
|
|
|
|
if (value == 'inherit')
|
|
{
|
|
referenced = graph.model.getParent(state.cell);
|
|
}
|
|
else if (value == 'swimlane')
|
|
{
|
|
shape[field] = (key == mxConstants.STYLE_STROKECOLOR ||
|
|
key == mxConstants.STYLE_FONTCOLOR) ?
|
|
'#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 != null)
|
|
{
|
|
shape[field] = state.shape.indicatorColor;
|
|
}
|
|
|
|
if (referenced != null)
|
|
{
|
|
var rstate = graph.getView().getState(referenced);
|
|
shape[field] = null;
|
|
|
|
if (rstate != null)
|
|
{
|
|
var rshape = (key == mxConstants.STYLE_FONTCOLOR) ? rstate.text : rstate.shape;
|
|
|
|
if (rshape != null && field != 'indicatorColor')
|
|
{
|
|
shape[field] = rshape[field];
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
this.resolveColor(state, 'color', mxConstants.STYLE_FONTCOLOR);
|
|
|
|
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 == null || !state.text.bounds.equals(bounds))
|
|
{
|
|
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);
|
|
}
|
|
|
|
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 != null &&
|
|
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;
|
|
}
|
|
|
|
// Updates indicator shape
|
|
if (state.shape != null && state.shape.indicatorShape !=
|
|
this.getShape(state.view.graph.getIndicatorShape(state)))
|
|
{
|
|
if (state.shape.indicator != null)
|
|
{
|
|
state.shape.indicator.destroy();
|
|
state.shape.indicator = null;
|
|
}
|
|
|
|
this.createIndicatorShape(state);
|
|
|
|
if (state.shape.indicatorShape != null)
|
|
{
|
|
state.shape.indicator = new state.shape.indicatorShape();
|
|
state.shape.indicator.dialect = state.shape.dialect;
|
|
state.shape.indicator.init(state.node);
|
|
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.
|
|
*
|
|
* state - <mxCellState> that represents the edge to be updated.
|
|
* sourceScaled - <mxCellState> that represents the source terminal.
|
|
* targetScaled - <mxCellState> that represents the target terminal.
|
|
* controlHints - List of relative control points.
|
|
* result - Array of <mxPoints> that represent the actual points of the
|
|
* edge.
|
|
*
|
|
*/
|
|
SegmentConnector: function(state, sourceScaled, targetScaled, controlHints, result)
|
|
{
|
|
// Creates array of all way- and terminalpoints
|
|
var pts = mxEdgeStyle.scalePointArray(state.absolutePoints, state.view.scale);
|
|
var source = mxEdgeStyle.scaleCellState(sourceScaled, state.view.scale);
|
|
var target = mxEdgeStyle.scaleCellState(targetScaled, state.view.scale);
|
|
var tol = 1;
|
|
|
|
// 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)
|
|
{
|
|
pt.x = Math.round(pt.x * state.view.scale * 10) / 10;
|
|
pt.y = Math.round(pt.y * state.view.scale * 10) / 10;
|
|
|
|
if (lastPushed == null || Math.abs(lastPushed.x - pt.x) >= tol || Math.abs(lastPushed.y - pt.y) >= Math.max(1, state.view.scale))
|
|
{
|
|
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();
|
|
}
|
|
|
|
var lastInx = pts.length - 1;
|
|
|
|
// Adds the waypoints
|
|
if (controlHints != null && controlHints.length > 0)
|
|
{
|
|
// Converts all hints and removes nulls
|
|
var hints = [];
|
|
|
|
for (var i = 0; i < controlHints.length; i++)
|
|
{
|
|
var tmp = state.view.transformControlPoint(state, controlHints[i], true);
|
|
|
|
if (tmp != null)
|
|
{
|
|
hints.push(tmp);
|
|
}
|
|
}
|
|
|
|
if (hints.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
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, 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: scalePointArray
|
|
*
|
|
* Scales an array of <mxPoint>
|
|
*
|
|
* Parameters:
|
|
*
|
|
* points - array of <mxPoint> to scale
|
|
* scale - the scaling to divide by
|
|
*
|
|
*/
|
|
scalePointArray: function(points, scale)
|
|
{
|
|
var result = [];
|
|
|
|
if (points != null)
|
|
{
|
|
for (var i = 0; i < points.length; i++)
|
|
{
|
|
if (points[i] != null)
|
|
{
|
|
var pt = new mxPoint(Math.round(points[i].x / scale * 10) / 10,
|
|
Math.round(points[i].y / scale * 10) / 10);
|
|
result[i] = pt;
|
|
}
|
|
else
|
|
{
|
|
result[i] = null;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = null;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Function: scaleCellState
|
|
*
|
|
* Scales an <mxCellState>
|
|
*
|
|
* Parameters:
|
|
*
|
|
* state - <mxCellState> to scale
|
|
* scale - the scaling to divide by
|
|
*
|
|
*/
|
|
scaleCellState: function(state, scale)
|
|
{
|
|
var result = null;
|
|
|
|
if (state != null)
|
|
{
|
|
result = state.clone();
|
|
result.setRect(Math.round(state.x / scale * 10) / 10,
|
|
Math.round(state.y / scale * 10) / 10,
|
|
Math.round(state.width / scale * 10) / 10,
|
|
Math.round(state.height / scale * 10) / 10);
|
|
}
|
|
else
|
|
{
|
|
result = null;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Function: OrthConnector
|
|
*
|
|
* Implements a local orthogonal router between the given
|
|
* cells.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* state - <mxCellState> that represents the edge to be updated.
|
|
* sourceScaled - <mxCellState> that represents the source terminal.
|
|
* targetScaled - <mxCellState> that represents the target terminal.
|
|
* controlHints - List of relative control points.
|
|
* result - Array of <mxPoints> that represent the actual points of the
|
|
* edge.
|
|
*
|
|
*/
|
|
OrthConnector: function(state, sourceScaled, targetScaled, controlHints, 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 = mxEdgeStyle.scalePointArray(state.absolutePoints, state.view.scale);
|
|
var source = mxEdgeStyle.scaleCellState(sourceScaled, state.view.scale);
|
|
var target = mxEdgeStyle.scaleCellState(targetScaled, state.view.scale);
|
|
|
|
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 sourceBuffer = mxEdgeStyle.getJettySize(state, true);
|
|
var targetBuffer = mxEdgeStyle.getJettySize(state, false);
|
|
|
|
//console.log('sourceBuffer', sourceBuffer);
|
|
//console.log('targetBuffer', targetBuffer);
|
|
// Workaround for loop routing within buffer zone
|
|
if (source != null && target == source)
|
|
{
|
|
targetBuffer = Math.max(sourceBuffer, targetBuffer);
|
|
sourceBuffer = targetBuffer;
|
|
}
|
|
|
|
var totalBuffer = targetBuffer + sourceBuffer;
|
|
// console.log('totalBuffer', totalBuffer);
|
|
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 && (controlHints != null &&
|
|
controlHints.length > 0)) || sourceEdge || targetEdge)
|
|
{
|
|
mxEdgeStyle.SegmentConnector(state, sourceScaled, targetScaled, controlHints, 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);
|
|
|
|
//console.log('source rotation', rotation);
|
|
|
|
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);
|
|
|
|
//console.log('target rotation', rotation);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
//console.log('source' , sourceX, sourceY, sourceWidth, sourceHeight);
|
|
//console.log('targetX' , targetX, targetY, targetWidth, targetHeight);
|
|
|
|
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 = [sourceBuffer, targetBuffer];
|
|
|
|
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;
|
|
|
|
// 0 | 1
|
|
// -----
|
|
// 3 | 2
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
//console.log('quad', quad);
|
|
|
|
// 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];
|
|
|
|
//console.log('routePattern', routePattern);
|
|
|
|
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] -= sourceBuffer;
|
|
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] + sourceBuffer;
|
|
break;
|
|
case mxConstants.DIRECTION_MASK_EAST:
|
|
mxEdgeStyle.wayPoints1[0][0] += geo[0][2] + sourceBuffer;
|
|
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] -= sourceBuffer;
|
|
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] * state.view.scale * 10) / 10,
|
|
Math.round(mxEdgeStyle.wayPoints1[i][1] * state.view.scale * 10) / 10));
|
|
}
|
|
|
|
//console.log(result);
|
|
|
|
// 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 != null) ? 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.unscaledWidth + offset.x;
|
|
state.origin.y += geo.y * pState.unscaledHeight + 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;
|
|
state.unscaledHeight = 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, false); // FIXME Rounding introduced bugs when calculating label positions -> , 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, ignoreScale)
|
|
{
|
|
if (state != null && pt != null)
|
|
{
|
|
var orig = state.origin;
|
|
var scale = ignoreScale ? 1 : this.scale
|
|
|
|
return new mxPoint(scale * (pt.x + this.translate.x + orig.x),
|
|
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-, font- 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.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* ignoreFn - Optional function that takes a change and returns true if the
|
|
* change should be ignored.
|
|
*
|
|
*/
|
|
mxGraph.prototype.getSelectionCellsForChanges = function(changes, ignoreFn)
|
|
{
|
|
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 &&
|
|
(ignoreFn == null || !ignoreFn(change)))
|
|
{
|
|
var cell = null;
|
|
|
|
if (change instanceof mxChildChange)
|
|
{
|
|
cell = change.child;
|
|
}
|
|
else if (!structureOnly && 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)
|
|
{
|
|
var temp = swimlane;
|
|
var swimlanes = [];
|
|
|
|
while (temp != null)
|
|
{
|
|
temp = this.model.getParent(temp);
|
|
var state = this.view.getState(temp);
|
|
|
|
if (this.isSwimlane(temp) && state != null &&
|
|
this.intersects(state, me.getGraphX(), me.getGraphY()))
|
|
{
|
|
swimlanes.push(temp);
|
|
}
|
|
}
|
|
|
|
// Selects ancestors for selected swimlanes
|
|
if (swimlanes.length > 0)
|
|
{
|
|
swimlanes = swimlanes.reverse();
|
|
swimlanes.splice(0, 0, swimlane);
|
|
swimlanes.push(swimlane);
|
|
|
|
for (var i = 0; i < swimlanes.length - 2; i++)
|
|
{
|
|
if (this.isCellSelected(swimlanes[i]))
|
|
{
|
|
swimlane = swimlanes[i + 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
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 + 2 * border;
|
|
var height = Math.max(0, bounds.y) + bounds.height + 2 * 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: getCurrentCellStyle
|
|
*
|
|
* Returns the style for the given cell from the cell state, if one exists,
|
|
* or using <getCellStyle>.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cell - <mxCell> whose style should be returned as an array.
|
|
* ignoreState - Optional boolean that specifies if the cell state should be ignored.
|
|
*/
|
|
mxGraph.prototype.getCurrentCellStyle = function(cell, ignoreState)
|
|
{
|
|
var state = (ignoreState) ? null : this.view.getState(cell);
|
|
|
|
return (state != null) ? state.style : this.getCellStyle(cell);
|
|
};
|
|
|
|
/**
|
|
* 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 style = this.getCurrentCellStyle(cells[0]);
|
|
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 style = this.getCurrentCellStyle(cells[0]);
|
|
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 style = this.getCurrentCellStyle(edge);
|
|
* 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.
|
|
* absolute - Optional boolean indicating of cells should be kept at
|
|
* their absolute position. Default is false.
|
|
*/
|
|
mxGraph.prototype.addCells = function(cells, parent, index, source, target, absolute)
|
|
{
|
|
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, (absolute != null) ? absolute : 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 style = this.getCurrentCellStyle(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 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.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.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, false);
|
|
}
|
|
|
|
value = value.replace(/\n/g, '<br>');
|
|
|
|
var size = mxUtils.getSizeForString(value, fontSize,
|
|
style[mxConstants.STYLE_FONTFAMILY], null,
|
|
style[mxConstants.STYLE_FONTSTYLE]);
|
|
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
|
|
{
|
|
var prev = this.cellsResized(cells, bounds, recurse);
|
|
this.fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS,
|
|
'cells', cells, 'bounds', bounds, 'previous', prev));
|
|
}
|
|
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;
|
|
var prev = [];
|
|
|
|
if (cells != null && bounds != null && cells.length == bounds.length)
|
|
{
|
|
this.model.beginUpdate();
|
|
try
|
|
{
|
|
for (var i = 0; i < cells.length; i++)
|
|
{
|
|
prev.push(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, 'previous', prev));
|
|
}
|
|
finally
|
|
{
|
|
this.model.endUpdate();
|
|
}
|
|
}
|
|
|
|
return prev;
|
|
};
|
|
|
|
/**
|
|
* 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 prev = this.model.getGeometry(cell);
|
|
|
|
if (prev != null && (prev.x != bounds.x || prev.y != bounds.y ||
|
|
prev.width != bounds.width || prev.height != bounds.height))
|
|
{
|
|
var geo = prev.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();
|
|
}
|
|
}
|
|
|
|
return prev;
|
|
};
|
|
|
|
/**
|
|
* 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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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;
|
|
}
|
|
|
|
var style = this.getCurrentCellStyle(cells[i]);
|
|
|
|
if (bbox != null)
|
|
{
|
|
var angle = mxUtils.getValue(style, mxConstants.STYLE_ROTATION, 0);
|
|
|
|
if (angle != 0)
|
|
{
|
|
bbox = mxUtils.getBoundingBox(bbox, angle);
|
|
}
|
|
}
|
|
}
|
|
|
|
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: snapDelta
|
|
*
|
|
* Snaps the given delta with the given scaled bounds.
|
|
*/
|
|
mxGraph.prototype.snapDelta = function(delta, bounds, ignoreGrid, ignoreHorizontal, ignoreVertical)
|
|
{
|
|
var t = this.view.translate;
|
|
var s = this.view.scale;
|
|
|
|
if (!ignoreGrid && this.gridEnabled)
|
|
{
|
|
var tol = this.gridSize * s * 0.5;
|
|
|
|
if (!ignoreHorizontal)
|
|
{
|
|
var tx = bounds.x - (this.snap(bounds.x / s - t.x) + t.x) * s;
|
|
|
|
if (Math.abs(delta.x- tx) < tol)
|
|
{
|
|
delta.x = 0;
|
|
}
|
|
else
|
|
{
|
|
delta.x = this.snap(delta.x / s) * s - tx;
|
|
}
|
|
}
|
|
|
|
if (!ignoreVertical)
|
|
{
|
|
var ty = bounds.y - (this.snap(bounds.y / s - t.y) + t.y) * s;
|
|
|
|
if (Math.abs(delta.y - ty) < tol)
|
|
{
|
|
delta.y = 0;
|
|
}
|
|
else
|
|
{
|
|
delta.y = this.snap(delta.y / s) * s - ty;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var tol = 0.5 * s;
|
|
|
|
if (!ignoreHorizontal)
|
|
{
|
|
var tx = bounds.x - (Math.round(bounds.x / s - t.x) + t.x) * s;
|
|
|
|
if (Math.abs(delta.x - tx) < tol)
|
|
{
|
|
delta.x = 0;
|
|
}
|
|
else
|
|
{
|
|
delta.x = Math.round(delta.x / s) * s - tx;
|
|
}
|
|
}
|
|
|
|
if (!ignoreVertical)
|
|
{
|
|
var ty = bounds.y - (Math.round(bounds.y / s - t.y) + t.y) * s;
|
|
|
|
if (Math.abs(delta.y - ty) < tol)
|
|
{
|
|
delta.y = 0;
|
|
}
|
|
else
|
|
{
|
|
delta.y = Math.round(delta.y / s) * s - ty;
|
|
}
|
|
}
|
|
}
|
|
|
|
return delta;
|
|
};
|
|
|
|
/**
|
|
* 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 style = this.getCurrentCellStyle(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)
|
|
{
|
|
return this.getCurrentCellStyle(cell)[mxConstants.STYLE_WHITE_SPACE] == 'wrap';
|
|
};
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
return this.getCurrentCellStyle(cell)[mxConstants.STYLE_OVERFLOW] == 'hidden';
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* ignoreState - Optional boolean that specifies if cell state should be ignored.
|
|
*/
|
|
mxGraph.prototype.getStartSize = function(swimlane, ignoreState)
|
|
{
|
|
var result = new mxRectangle();
|
|
var style = this.getCurrentCellStyle(swimlane, ignoreState);
|
|
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: isTransparentState
|
|
*
|
|
* Returns true if the given state has no stroke- or fillcolor and no image.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* state - <mxCellState> to check.
|
|
*/
|
|
mxGraph.prototype.isTransparentState = function(state)
|
|
{
|
|
var result = false;
|
|
|
|
if (state != null)
|
|
{
|
|
var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE);
|
|
var fill = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE);
|
|
|
|
result = stroke == mxConstants.NONE && fill == mxConstants.NONE && this.getImage(state) == null;
|
|
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* 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 && this.model.getParent(cell) != this.model.getRoot() && !this.model.isEdge(cell))
|
|
{
|
|
return this.getCurrentCellStyle(cell)[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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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 style = this.getCurrentCellStyle(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)
|
|
{
|
|
if (parent == null)
|
|
{
|
|
parent = this.getCurrentRoot();
|
|
|
|
if (parent == null)
|
|
{
|
|
parent = this.model.getRoot();
|
|
}
|
|
}
|
|
|
|
if (parent != null)
|
|
{
|
|
var childCount = this.model.getChildCount(parent);
|
|
|
|
for (var i = 0; i < childCount; i++)
|
|
{
|
|
var child = this.model.getChildAt(parent, i);
|
|
|
|
if (child != null)
|
|
{
|
|
var result = this.getSwimlaneAt(x, y, child);
|
|
|
|
if (result != null)
|
|
{
|
|
return result;
|
|
}
|
|
else if (this.isCellVisible(child) && 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.
|
|
* sources - 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, selectGroups)
|
|
{
|
|
this.selectCells(true, false, parent, selectGroups);
|
|
};
|
|
|
|
/**
|
|
* 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>.
|
|
* selectGroups - Optional boolean that specifies if groups should be
|
|
* selected. Default is false.
|
|
*/
|
|
mxGraph.prototype.selectCells = function(vertices, edges, parent, selectGroups)
|
|
{
|
|
parent = parent || this.getDefaultParent();
|
|
|
|
var filter = mxUtils.bind(this, function(cell)
|
|
{
|
|
return this.view.getState(cell) != null &&
|
|
(((selectGroups || 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 && mxClient.IOS_VERSION <= 12 && 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, eventName)
|
|
* {
|
|
* return layout;
|
|
* };
|
|
* (end)
|
|
*
|
|
* See <getLayout> for a description of the possible eventNames.
|
|
*
|
|
* 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'));
|
|
}
|
|
});
|
|
|
|
// Notifies the layout of a move operation inside a parent
|
|
this.resizeHandler = mxUtils.bind(this, function(sender, evt)
|
|
{
|
|
if (this.isEnabled())
|
|
{
|
|
this.cellsResized(evt.getProperty('cells'), evt.getProperty('bounds'),
|
|
evt.getProperty('previous'));
|
|
}
|
|
});
|
|
|
|
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: undoHandler
|
|
*
|
|
* Holds the function that handles the endUpdate event.
|
|
*/
|
|
mxLayoutManager.prototype.undoHandler = null;
|
|
|
|
/**
|
|
* Variable: moveHandler
|
|
*
|
|
* Holds the function that handles the move event.
|
|
*/
|
|
mxLayoutManager.prototype.moveHandler = null;
|
|
|
|
/**
|
|
* Variable: resizeHandler
|
|
*
|
|
* Holds the function that handles the resize event.
|
|
*/
|
|
mxLayoutManager.prototype.resizeHandler = 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.removeListener(this.resizeHandler);
|
|
}
|
|
|
|
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);
|
|
this.graph.addListener(mxEvent.RESIZE_CELLS, this.resizeHandler);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: getLayout
|
|
*
|
|
* Returns the layout for the given cell and eventName. Possible
|
|
* event names are <mxEvent.MOVE_CELLS> and <mxEvent.RESIZE_CELLS>
|
|
* for callbacks on when cells are moved or resized and
|
|
* <mxEvent.BEGIN_UPDATE> and <mxEvent.END_UPDATE> for the capture
|
|
* and bubble phase of the layout after any changes of the model.
|
|
*/
|
|
mxLayoutManager.prototype.getLayout = function(cell, eventName)
|
|
{
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Function: beforeUndo
|
|
*
|
|
* Called from <undoHandler>.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cell - Array of <mxCells> that have been moved.
|
|
* evt - Mouse event that represents the mousedown.
|
|
*/
|
|
mxLayoutManager.prototype.beforeUndo = function(undoableEdit)
|
|
{
|
|
this.executeLayoutForCells(this.getCellsForChanges(undoableEdit.changes));
|
|
};
|
|
|
|
/**
|
|
* Function: cellsMoved
|
|
*
|
|
* Called from <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));
|
|
|
|
// 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 layout = this.getAncestorLayout(cells[i], mxEvent.MOVE_CELLS);
|
|
|
|
if (layout != null)
|
|
{
|
|
layout.moveCell(cells[i], point.x, point.y);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: cellsResized
|
|
*
|
|
* Called from <resizeHandler>.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cell - Array of <mxCells> that have been resized.
|
|
* bounds - <mxRectangle> taht represents the new bounds.
|
|
*/
|
|
mxLayoutManager.prototype.cellsResized = function(cells, bounds, prev)
|
|
{
|
|
if (cells != null && bounds != null)
|
|
{
|
|
// Checks if a layout exists to take care of the resize if the
|
|
// parent itself is not being resized
|
|
for (var i = 0; i < cells.length; i++)
|
|
{
|
|
var layout = this.getAncestorLayout(cells[i], mxEvent.RESIZE_CELLS);
|
|
|
|
if (layout != null)
|
|
{
|
|
layout.resizeCell(cells[i], bounds[i], prev[i]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: getAncestorLayout
|
|
*
|
|
* Returns the cells to be layouted for the given sequence of changes.
|
|
*/
|
|
mxLayoutManager.prototype.getAncestorLayout = function(cell, eventName)
|
|
{
|
|
var model = this.getGraph().getModel();
|
|
|
|
while (cell != null)
|
|
{
|
|
var layout = this.getLayout(cell, eventName);
|
|
|
|
if (layout != null)
|
|
{
|
|
return layout;
|
|
}
|
|
|
|
cell = model.getParent(cell);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Function: getCellsForChanges
|
|
*
|
|
* Returns the cells for which a layout should be executed.
|
|
*/
|
|
mxLayoutManager.prototype.getCellsForChanges = function(changes)
|
|
{
|
|
var result = [];
|
|
|
|
for (var i = 0; i < changes.length; i++)
|
|
{
|
|
var change = changes[i];
|
|
|
|
if (change instanceof mxRootChange)
|
|
{
|
|
return [];
|
|
}
|
|
else
|
|
{
|
|
result = result.concat(this.getCellsForChange(change));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Function: getCellsForChange
|
|
*
|
|
* Executes all layouts which have been scheduled during the
|
|
* changes.
|
|
*/
|
|
mxLayoutManager.prototype.getCellsForChange = function(change)
|
|
{
|
|
if (change instanceof mxChildChange)
|
|
{
|
|
return this.addCellsWithLayout(change.child,
|
|
this.addCellsWithLayout(change.previous));
|
|
}
|
|
else if (change instanceof mxTerminalChange ||
|
|
change instanceof mxGeometryChange)
|
|
{
|
|
return this.addCellsWithLayout(change.cell);
|
|
}
|
|
else if (change instanceof mxVisibleChange ||
|
|
change instanceof mxStyleChange)
|
|
{
|
|
return this.addCellsWithLayout(change.cell);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
/**
|
|
* Function: addCellsWithLayout
|
|
*
|
|
* Adds all ancestors of the given cell that have a layout.
|
|
*/
|
|
mxLayoutManager.prototype.addCellsWithLayout = function(cell, result)
|
|
{
|
|
return this.addDescendantsWithLayout(cell,
|
|
this.addAncestorsWithLayout(cell, result));
|
|
};
|
|
|
|
/**
|
|
* Function: addAncestorsWithLayout
|
|
*
|
|
* Adds all ancestors of the given cell that have a layout.
|
|
*/
|
|
mxLayoutManager.prototype.addAncestorsWithLayout = function(cell, result)
|
|
{
|
|
result = (result != null) ? result : [];
|
|
|
|
if (cell != null)
|
|
{
|
|
var layout = this.getLayout(cell);
|
|
|
|
if (layout != null)
|
|
{
|
|
result.push(cell);
|
|
}
|
|
|
|
if (this.isBubbling())
|
|
{
|
|
var model = this.getGraph().getModel();
|
|
this.addAncestorsWithLayout(
|
|
model.getParent(cell), result);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Function: addDescendantsWithLayout
|
|
*
|
|
* Adds all descendants of the given cell that have a layout.
|
|
*/
|
|
mxLayoutManager.prototype.addDescendantsWithLayout = function(cell, result)
|
|
{
|
|
result = (result != null) ? result : [];
|
|
|
|
if (cell != null && this.getLayout(cell) != null)
|
|
{
|
|
var model = this.getGraph().getModel();
|
|
|
|
for (var i = 0; i < model.getChildCount(cell); i++)
|
|
{
|
|
var child = model.getChildAt(cell, i);
|
|
|
|
if (this.getLayout(child) != null)
|
|
{
|
|
result.push(child);
|
|
this.addDescendantsWithLayout(child, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Function: executeLayoutForCells
|
|
*
|
|
* Executes the given layout on the given parent.
|
|
*/
|
|
mxLayoutManager.prototype.executeLayoutForCells = function(cells)
|
|
{
|
|
// Adds reverse to this array to avoid duplicate execution of leaves
|
|
// 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);
|
|
this.layoutCells(sorted, false);
|
|
this.layoutCells(sorted.reverse(), true);
|
|
};
|
|
|
|
/**
|
|
* Function: layoutCells
|
|
*
|
|
* Executes all layouts which have been scheduled during the changes.
|
|
*/
|
|
mxLayoutManager.prototype.layoutCells = function(cells, bubble)
|
|
{
|
|
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)
|
|
{
|
|
this.executeLayout(cells[i], bubble);
|
|
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(cell, bubble)
|
|
{
|
|
var layout = this.getLayout(cell, (bubble) ?
|
|
mxEvent.END_UPDATE : mxEvent.BEGIN_UPDATE);
|
|
|
|
if (layout != null)
|
|
{
|
|
layout.execute(cell);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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()
|
|
{
|
|
if (!this.suspended)
|
|
{
|
|
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)
|
|
{
|
|
// Waits for the states and handlers to be updated
|
|
window.setTimeout(mxUtils.bind(this, function()
|
|
{
|
|
if (this.first != null && !this.suspended)
|
|
{
|
|
// Updates preview with no translate to compute bounding box
|
|
var dx = this.currentDx;
|
|
var dy = this.currentDy;
|
|
this.currentDx = 0;
|
|
this.currentDy = 0;
|
|
this.updatePreview();
|
|
this.bounds = this.graph.getView().getBounds(this.cells);
|
|
this.pBounds = this.getPreviewBounds(this.cells);
|
|
|
|
if (this.pBounds == null)
|
|
{
|
|
this.reset();
|
|
}
|
|
else
|
|
{
|
|
// Restores translate and updates preview
|
|
this.currentDx = dx;
|
|
this.currentDy = dy;
|
|
this.updatePreview();
|
|
this.updateHint();
|
|
|
|
if (this.livePreviewUsed)
|
|
{
|
|
this.setHandlesVisibleForCells(this.graph.getSelectionCells(), false);
|
|
}
|
|
}
|
|
}
|
|
}), 0);
|
|
});
|
|
|
|
this.graph.getModel().addListener(mxEvent.CHANGE, this.refreshHandler);
|
|
|
|
this.keyHandler = mxUtils.bind(this, function(e)
|
|
{
|
|
if (this.graph.container != null && this.graph.container.style.visibility != 'hidden' &&
|
|
this.first != null && !this.suspended)
|
|
{
|
|
var clone = this.graph.isCloneEvent(e) &&
|
|
this.graph.isCellsCloneable() &&
|
|
this.isCloneEnabled();
|
|
|
|
if (clone != this.cloning)
|
|
{
|
|
this.cloning = clone;
|
|
this.checkPreview();
|
|
this.updatePreview();
|
|
}
|
|
}
|
|
});
|
|
|
|
mxEvent.addListener(document, 'keydown', this.keyHandler);
|
|
mxEvent.addListener(document, 'keyup', this.keyHandler);
|
|
};
|
|
|
|
/**
|
|
* 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: handlesVisible
|
|
*
|
|
* Whether the handles of the selection are currently visible.
|
|
*/
|
|
mxGraphHandler.prototype.handlesVisible = true;
|
|
|
|
/**
|
|
* 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, cells)
|
|
{
|
|
this.cell = cell;
|
|
this.first = mxUtils.convertPoint(this.graph.container, x, y);
|
|
this.cells = (cells != null) ? 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;
|
|
|
|
// Uses connected states as guides
|
|
var connected = new mxDictionary();
|
|
var opps = this.graph.getOpposites(this.graph.getEdges(this.cell), this.cell);
|
|
|
|
for (var i = 0; i < opps.length; i++)
|
|
{
|
|
var state = this.graph.view.getState(opps[i]);
|
|
|
|
if (state != null && !connected.get(state))
|
|
{
|
|
connected.put(state, true);
|
|
}
|
|
}
|
|
|
|
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 &&
|
|
!connected.get(state) &&
|
|
(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());
|
|
|
|
return new mxPoint(point.x - this.first.x - this.graph.panDx,
|
|
point.y - this.first.y - this.graph.panDy);
|
|
};
|
|
|
|
/**
|
|
* 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. Allows for half steps in the raster so
|
|
* numbers coming in should be rounded if no half steps are allowed (ie for non
|
|
* aligned standard moving where pixel steps should be preferred).
|
|
*/
|
|
mxGraphHandler.prototype.roundLength = function(length)
|
|
{
|
|
return Math.round(length * 100) / 100;
|
|
};
|
|
|
|
/**
|
|
* Function: isValidDropTarget
|
|
*
|
|
* Returns true if the given cell is a valid drop target.
|
|
*/
|
|
mxGraphHandler.prototype.isValidDropTarget = function(target)
|
|
{
|
|
return this.graph.model.getParent(this.cell) != target;
|
|
};
|
|
|
|
/**
|
|
* Function: checkPreview
|
|
*
|
|
* Updates the preview if cloning state has changed.
|
|
*/
|
|
mxGraphHandler.prototype.checkPreview = function()
|
|
{
|
|
if (this.livePreviewActive && this.cloning)
|
|
{
|
|
this.resetLivePreview();
|
|
this.livePreviewActive = false;
|
|
}
|
|
else if (this.maxLivePreview >= this.cellCount && !this.livePreviewActive && this.allowLivePreview)
|
|
{
|
|
if (!this.cloning || !this.livePreviewActive)
|
|
{
|
|
this.livePreviewActive = true;
|
|
this.livePreviewUsed = true;
|
|
}
|
|
}
|
|
else if (!this.livePreviewUsed && this.shape == null)
|
|
{
|
|
this.shape = this.createPreviewShape(this.bounds);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 && !this.suspended)
|
|
{
|
|
// Stops moving if a multi touch event is received
|
|
if (mxEvent.isMultiTouchEvent(me.getEvent()))
|
|
{
|
|
this.reset();
|
|
return;
|
|
}
|
|
|
|
var delta = this.getDelta(me);
|
|
var tol = graph.tolerance;
|
|
|
|
if (this.shape != null || this.livePreviewActive || Math.abs(delta.x) > tol || Math.abs(delta.y) > 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 && (clone || this.isValidDropTarget(target)))
|
|
{
|
|
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.guide != null && this.useGuidesForEvent(me))
|
|
{
|
|
delta = this.guide.move(this.bounds, delta, gridEnabled, clone);
|
|
hideGuide = false;
|
|
}
|
|
else
|
|
{
|
|
delta = this.graph.snapDelta(delta, this.bounds, !gridEnabled, false, false);
|
|
}
|
|
|
|
if (this.guide != null && hideGuide)
|
|
{
|
|
this.guide.hide();
|
|
}
|
|
|
|
// Constrained movement if shift key is pressed
|
|
if (graph.isConstrainedEvent(me.getEvent()))
|
|
{
|
|
if (Math.abs(delta.x) > Math.abs(delta.y))
|
|
{
|
|
delta.y = 0;
|
|
}
|
|
else
|
|
{
|
|
delta.x = 0;
|
|
}
|
|
}
|
|
|
|
this.checkPreview();
|
|
|
|
if (this.currentDx != delta.x || this.currentDy != delta.y)
|
|
{
|
|
this.currentDx = delta.x;
|
|
this.currentDy = delta.y;
|
|
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.setHandlesVisibleForCells(this.graph.getSelectionCells(), false);
|
|
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.pBounds != 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)
|
|
{
|
|
if (!this.suspended)
|
|
{
|
|
var states = [];
|
|
|
|
if (this.allCells != null)
|
|
{
|
|
this.allCells.visit(mxUtils.bind(this, function(key, state)
|
|
{
|
|
// Checks if cell was removed
|
|
if (this.graph.view.getState(state.cell) == null)
|
|
{
|
|
state.destroy();
|
|
}
|
|
else
|
|
{
|
|
// 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)
|
|
{
|
|
if (state.text.originalPointerEvents == null)
|
|
{
|
|
state.text.originalPointerEvents = state.text.pointerEvents;
|
|
}
|
|
|
|
state.text.pointerEvents = false;
|
|
}
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Resets the handler if everything was removed
|
|
if (states.length == 0)
|
|
{
|
|
this.reset();
|
|
}
|
|
else
|
|
{
|
|
// 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.view.updateEdgeLabelOffset(state);
|
|
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: suspend
|
|
*
|
|
* Suspends the livew preview.
|
|
*/
|
|
mxGraphHandler.prototype.suspend = function()
|
|
{
|
|
if (!this.suspended)
|
|
{
|
|
if (this.livePreviewUsed)
|
|
{
|
|
this.updateLivePreview(0, 0);
|
|
}
|
|
|
|
if (this.shape != null)
|
|
{
|
|
this.shape.node.style.visibility = 'hidden';
|
|
}
|
|
|
|
if (this.guide != null)
|
|
{
|
|
this.guide.setVisible(false);
|
|
}
|
|
|
|
this.suspended = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: resume
|
|
*
|
|
* Suspends the livew preview.
|
|
*/
|
|
mxGraphHandler.prototype.resume = function()
|
|
{
|
|
if (this.suspended)
|
|
{
|
|
this.suspended = null;
|
|
|
|
if (this.livePreviewUsed)
|
|
{
|
|
this.livePreviewActive = true;
|
|
}
|
|
|
|
if (this.shape != null)
|
|
{
|
|
this.shape.node.style.visibility = 'visible';
|
|
}
|
|
|
|
if (this.guide != null)
|
|
{
|
|
this.guide.setVisible(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 repaint even if not moved to update pointer events
|
|
state.shape.bounds = null;
|
|
|
|
if (state.text != null)
|
|
{
|
|
state.text.pointerEvents = state.text.originalPointerEvents;
|
|
state.text.originalPointerEvents = null;
|
|
}
|
|
}
|
|
|
|
// Shows folding icon
|
|
if (state.control != null && state.control.node != null &&
|
|
state.control.node.style.visibility == 'hidden')
|
|
{
|
|
state.control.node.style.visibility = '';
|
|
}
|
|
|
|
// Forces repaint of 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)
|
|
{
|
|
if (this.handlesVisible != visible)
|
|
{
|
|
this.handlesVisible = 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.graph.getSelectionCells(), true);
|
|
}
|
|
|
|
this.destroyShapes();
|
|
this.removeHint();
|
|
|
|
this.delayedSelection = false;
|
|
this.livePreviewActive = null;
|
|
this.livePreviewUsed = null;
|
|
this.cellWasClicked = false;
|
|
this.suspended = null;
|
|
this.currentDx = null;
|
|
this.currentDy = null;
|
|
this.cellCount = null;
|
|
this.cloning = false;
|
|
this.allCells = null;
|
|
this.pBounds = 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: shouldRemoveParent
|
|
*
|
|
* Returns true if the given parent should be removed after removal of child cells.
|
|
*/
|
|
mxGraphHandler.prototype.shouldRemoveParent = function(parent)
|
|
{
|
|
var state = this.graph.view.getState(parent);
|
|
|
|
return 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 &&
|
|
this.graph.isTransparentState(state);
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
mxEvent.removeListener(document, 'keydown', this.keyHandler);
|
|
mxEvent.removeListener(document, 'keyup', this.keyHandler);
|
|
|
|
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)
|
|
{
|
|
this.zoomGraph(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: zoomGraph
|
|
*
|
|
* Zooms the graph to the given value and consumed the event if needed.
|
|
*/
|
|
mxPanningHandler.prototype.zoomGraph = function(evt)
|
|
{
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
if (this.error != null || (this.currentState != null &&
|
|
!this.isCellEnabled(this.currentState.cell)))
|
|
{
|
|
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.constraintHandler.reset();
|
|
this.marker.reset();
|
|
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
|
|
// but ignores cases where parent is used for non-connectable child cells
|
|
if (this.graph.isCellConnectable(me.getCell()) &&
|
|
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 allows new connections to be created. This implementation
|
|
* always returns true.
|
|
*/
|
|
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(
|
|
mxRectangle.fromRectangle(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());
|
|
}
|
|
}
|
|
|
|
if (index == mxEvent.ROTATION_HANDLE)
|
|
{
|
|
// With the rotation handle in a corner, need the angle and distance
|
|
var pos = this.getRotationHandlePosition();
|
|
|
|
var dx = pos.x - this.state.getCenterX();
|
|
var dy = pos.y - this.state.getCenterY();
|
|
|
|
this.startAngle = (dx != 0) ? Math.atan(dy / dx) * 180 / Math.PI + 90 : ((dy < 0) ? 180 : 0);
|
|
this.startDist = Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
// 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 * 100) / 100;
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
this.currentAlpha -= this.startAngle;
|
|
|
|
// 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.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist - this.startDist < 2)
|
|
{
|
|
raster = 15;
|
|
}
|
|
else if (dist - this.startDist < 25)
|
|
{
|
|
raster = 5;
|
|
}
|
|
else
|
|
{
|
|
raster = 1;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
var old = this.bounds;
|
|
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 (!old.equals(this.bounds))
|
|
{
|
|
if (this.livePreviewActive)
|
|
{
|
|
this.updateLivePreview(me);
|
|
}
|
|
|
|
if (this.preview != null)
|
|
{
|
|
this.drawPreview();
|
|
}
|
|
else
|
|
{
|
|
this.updateParentHighlight();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// 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();
|
|
|
|
// Moves live preview to front
|
|
if ((this.state.text != null && this.state.text.node != null &&
|
|
this.state.text.node.nextSibling != null) ||
|
|
(this.state.shape != null && this.state.shape.node != null &&
|
|
this.state.shape.node.nextSibling != null && (this.state.text == null ||
|
|
this.state.shape.node.nextSibling != this.state.text.node)))
|
|
{
|
|
if (this.state.shape != null && this.state.shape.node != null)
|
|
{
|
|
this.state.shape.node.parentNode.appendChild(this.state.shape.node);
|
|
}
|
|
|
|
if (this.state.text != null && this.state.text.node != null)
|
|
{
|
|
this.state.text.node.parentNode.appendChild(this.state.text.node);
|
|
}
|
|
}
|
|
|
|
// 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 style = this.graph.getCurrentCellStyle(cell);
|
|
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 alpha = -mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION] || '0');
|
|
var cos = Math.cos(alpha);
|
|
var sin = Math.sin(alpha);
|
|
var scale = this.graph.view.scale;
|
|
var pt = mxUtils.getRotatedPoint(new mxPoint(
|
|
Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale),
|
|
Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale)),
|
|
cos, sin);
|
|
|
|
geo = geo.clone();
|
|
|
|
if (geo.offset == null)
|
|
{
|
|
geo.offset = pt;
|
|
}
|
|
else
|
|
{
|
|
geo.offset.x += pt.x;
|
|
geo.offset.y += pt.y;
|
|
}
|
|
|
|
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)
|
|
{
|
|
gridEnabled = (gridEnabled != null) ? gridEnabled && this.graph.gridEnabled : this.graph.gridEnabled;
|
|
|
|
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
|
|
{
|
|
bottom = Math.round(bottom / scale) * scale;
|
|
}
|
|
}
|
|
else if (index < 3 /* Top Row */)
|
|
{
|
|
top = top + dy;
|
|
|
|
if (gridEnabled)
|
|
{
|
|
top = this.graph.snap(top / scale) * scale;
|
|
}
|
|
else
|
|
{
|
|
top = Math.round(top / scale) * scale;
|
|
}
|
|
}
|
|
|
|
if (index == 0 || index == 3 || index == 5 /* Left */)
|
|
{
|
|
left += dx;
|
|
|
|
if (gridEnabled)
|
|
{
|
|
left = this.graph.snap(left / scale) * scale;
|
|
}
|
|
else
|
|
{
|
|
left = Math.round(left / scale) * scale;
|
|
}
|
|
}
|
|
else if (index == 2 || index == 4 || index == 7 /* Right */)
|
|
{
|
|
right += dx;
|
|
|
|
if (gridEnabled)
|
|
{
|
|
right = this.graph.snap(right / scale) * scale;
|
|
}
|
|
else
|
|
{
|
|
right = Math.round(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.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.isCustomHandleVisible(this.customHandles[i])) ?
|
|
'' : 'hidden';
|
|
}
|
|
}
|
|
|
|
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)]);
|
|
|
|
pt.x = cx + this.state.absoluteOffset.x;
|
|
pt.y = cy + this.state.absoluteOffset.y;
|
|
pt = mxUtils.getRotatedPoint(pt, cos, sin, ct);
|
|
this.moveSizerTo(this.sizers[8], pt.x, pt.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();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: isCustomHandleVisible
|
|
*
|
|
* Returns true if the given custom handle is visible.
|
|
*/
|
|
mxVertexHandler.prototype.isCustomHandleVisible = function(handle)
|
|
{
|
|
return !this.graph.isEditing() && this.state.view.graph.getSelectionCount() == 1;
|
|
};
|
|
|
|
/**
|
|
* 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 = mxRectangle.fromRectangle(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());
|
|
this.parentHighlight.redraw();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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();
|
|
this.updateParentHighlight();
|
|
};
|
|
|
|
/**
|
|
* 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: isCellEnabled
|
|
*
|
|
* Returns true if the given cell allows new connections to be created. This implementation
|
|
* always returns true.
|
|
*/
|
|
mxEdgeHandler.prototype.isCellEnabled = function(cell)
|
|
{
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* 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 != 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;
|
|
}
|
|
|
|
if (this.error != null || (result != null &&
|
|
!this.isCellEnabled(result.cell)))
|
|
{
|
|
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.isCellEnabled(state.cell))
|
|
{
|
|
this.constraintHandler.reset();
|
|
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.graph.isCellConnectable(me.getCell()) &&
|
|
this.marker.getValidState() != me.getState()) ?
|
|
'transparent' : mxConstants.DEFAULT_VALID_COLOR;
|
|
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.graph.isCellConnectable(me.getCell()) &&
|
|
this.marker.highlight.shape != null)
|
|
{
|
|
this.marker.highlight.shape.stroke = 'transparent';
|
|
this.marker.highlight.repaint();
|
|
terminalState = null;
|
|
}
|
|
}
|
|
|
|
if (terminalState != null && !this.isCellEnabled(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);
|
|
|
|
if (g != null)
|
|
{
|
|
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++)
|
|
{
|
|
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.isCustomHandleVisible(this.customHandles[i])) ?
|
|
'' : 'hidden';
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: isCustomHandleVisible
|
|
*
|
|
* Returns true if the given custom handle is visible.
|
|
*/
|
|
mxEdgeHandler.prototype.isCustomHandleVisible = function(handle)
|
|
{
|
|
return !this.graph.isEditing() && this.state.view.graph.getSelectionCount() == 1;
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
if (!this.labelShape.bounds.equals(bounds))
|
|
{
|
|
this.labelShape.bounds = bounds;
|
|
this.labelShape.redraw();
|
|
}
|
|
}
|
|
|
|
if (this.shape != null && !mxUtils.equalPoints(this.shape.points, this.abspoints))
|
|
{
|
|
this.shape.apply(this.state);
|
|
this.shape.points = this.abspoints.slice();
|
|
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.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 an 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", "
")
|
|
*
|
|
* 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.
|
|
*
|
|
* - < (<)
|
|
* - > (>)
|
|
* - & (&)
|
|
* - " (")
|
|
* - ' (')
|
|
*
|
|
* Although it is part of the XML language, ' is not defined in HTML.
|
|
* For this reason the XHTML specification recommends instead the use of
|
|
* ' 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 '
'.
|
|
*/
|
|
mxEditor.prototype.linefeed = '
';
|
|
|
|
/**
|
|
* 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;
|
|
|
|
}());
|