/**
* Copyright (c) 2006-2012, JGraph Ltd
*/
// Workaround for handling named HTML entities in mxUtils.parseXml
// LATER: How to configure DOMParser to just ignore all entities?
(function()
{
var entities = [
['nbsp', '160'],
['shy', '173']
];
var parseXml = mxUtils.parseXml;
mxUtils.parseXml = function(text)
{
for (var i = 0; i < entities.length; i++)
{
text = text.replace(new RegExp(
'&' + entities[i][0] + ';', 'g'),
'' + entities[i][1] + ';');
}
return parseXml(text);
};
})();
// Shim for missing toISOString in older versions of IE
// See https://stackoverflow.com/questions/12907862
if (!Date.prototype.toISOString)
{
(function()
{
function pad(number)
{
var r = String(number);
if (r.length === 1)
{
r = '0' + r;
}
return r;
};
Date.prototype.toISOString = function()
{
return this.getUTCFullYear()
+ '-' + pad( this.getUTCMonth() + 1 )
+ '-' + pad( this.getUTCDate() )
+ 'T' + pad( this.getUTCHours() )
+ ':' + pad( this.getUTCMinutes() )
+ ':' + pad( this.getUTCSeconds() )
+ '.' + String( (this.getUTCMilliseconds()/1000).toFixed(3) ).slice( 2, 5 )
+ 'Z';
};
}());
}
// Shim for Date.now()
if (!Date.now)
{
Date.now = function()
{
return new Date().getTime();
};
}
// Polyfill for Uint8Array.from in IE11 used in Graph.decompress
// See https://stackoverflow.com/questions/36810940/alternative-or-polyfill-for-array-from-on-the-internet-explorer
if (!Uint8Array.from) {
Uint8Array.from = (function () {
var toStr = Object.prototype.toString;
var isCallable = function (fn) {
return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
};
var toInteger = function (value) {
var number = Number(value);
if (isNaN(number)) { return 0; }
if (number === 0 || !isFinite(number)) { return number; }
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
};
var maxSafeInteger = Math.pow(2, 53) - 1;
var toLength = function (value) {
var len = toInteger(value);
return Math.min(Math.max(len, 0), maxSafeInteger);
};
// The length property of the from method is 1.
return function from(arrayLike/*, mapFn, thisArg */) {
// 1. Let C be the this value.
var C = this;
// 2. Let items be ToObject(arrayLike).
var items = Object(arrayLike);
// 3. ReturnIfAbrupt(items).
if (arrayLike == null) {
throw new TypeError("Array.from requires an array-like object - not null or undefined");
}
// 4. If mapfn is undefined, then let mapping be false.
var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
var T;
if (typeof mapFn !== 'undefined') {
// 5. else
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
if (!isCallable(mapFn)) {
throw new TypeError('Array.from: when provided, the second argument must be a function');
}
// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 2) {
T = arguments[2];
}
}
// 10. Let lenValue be Get(items, "length").
// 11. Let len be ToLength(lenValue).
var len = toLength(items.length);
// 13. If IsConstructor(C) is true, then
// 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
// 14. a. Else, Let A be ArrayCreate(len).
var A = isCallable(C) ? Object(new C(len)) : new Array(len);
// 16. Let k be 0.
var k = 0;
// 17. Repeat, while k < len… (also steps a - h)
var kValue;
while (k < len) {
kValue = items[k];
if (mapFn) {
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
} else {
A[k] = kValue;
}
k += 1;
}
// 18. Let putStatus be Put(A, "length", len, true).
A.length = len;
// 20. Return A.
return A;
};
}());
}
/**
* Measurements Units
*/
mxConstants.POINTS = 1;
mxConstants.MILLIMETERS = 2;
mxConstants.INCHES = 3;
mxConstants.METERS = 4;
/**
* This ratio is with page scale 1
*/
mxConstants.PIXELS_PER_MM = 3.937;
mxConstants.PIXELS_PER_INCH = 100;
mxConstants.SHADOW_OPACITY = 0.25;
mxConstants.SHADOWCOLOR = '#000000';
mxConstants.VML_SHADOWCOLOR = '#d0d0d0';
mxCodec.allowlist = ['mxStylesheet', 'Array', 'mxGraphModel',
'mxCell', 'mxGeometry', 'mxRectangle', 'mxPoint',
'mxChildChange', 'mxRootChange', 'mxTerminalChange',
'mxValueChange', 'mxStyleChange', 'mxGeometryChange',
'mxCollapseChange', 'mxVisibleChange', 'mxCellAttributeChange'];
mxGraph.prototype.pageBreakColor = '#c0c0c0';
mxGraph.prototype.pageScale = 1;
// Letter page format is default in US, Canada and Mexico
(function()
{
try
{
if (navigator != null && navigator.language != null)
{
var lang = navigator.language.toLowerCase();
mxGraph.prototype.pageFormat = (lang === 'en-us' || lang === 'en-ca' || lang === 'es-mx') ?
mxConstants.PAGE_FORMAT_LETTER_PORTRAIT : mxConstants.PAGE_FORMAT_A4_PORTRAIT;
}
}
catch (e)
{
// ignore
}
})();
// Matches label positions of mxGraph 1.x
mxText.prototype.baseSpacingTop = 5;
mxText.prototype.baseSpacingBottom = 1;
// Keeps edges between relative child cells inside parent
mxGraphModel.prototype.ignoreRelativeEdgeParent = false;
// Defines grid properties
mxGraphView.prototype.gridImage = (mxClient.IS_SVG) ? 'data:image/gif;base64,R0lGODlhCgAKAJEAAAAAAP///8zMzP///yH5BAEAAAMALAAAAAAKAAoAAAIJ1I6py+0Po2wFADs=' :
IMAGE_PATH + '/grid.gif';
mxGraphView.prototype.gridSteps = 4;
mxGraphView.prototype.minGridSize = 4;
// UrlParams is null in embed mode
mxGraphView.prototype.defaultGridColor = '#d0d0d0';
mxGraphView.prototype.defaultDarkGridColor = '#424242';
mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultGridColor;
// Units
mxGraphView.prototype.unit = mxConstants.POINTS;
mxGraphView.prototype.setUnit = function(unit)
{
if (this.unit != unit)
{
this.unit = unit;
this.fireEvent(new mxEventObject('unitChanged', 'unit', unit));
}
};
// Alternative text for unsupported foreignObjects
mxSvgCanvas2D.prototype.foAltText = '[Not supported by viewer]';
// Hook for custom constraints
mxShape.prototype.getConstraints = function(style, w, h)
{
return null;
};
// Override for clipSvg style
mxImageShape.prototype.getImageDataUri = function()
{
var src = this.image;
if (src.substring(0, 26) == 'data:image/svg+xml;base64,' && this.style != null &&
mxUtils.getValue(this.style, 'clipSvg', '0') == '1')
{
if (this.clippedSvg == null || this.clippedImage != src)
{
this.clippedSvg = Graph.clipSvgDataUri(src, true);
this.clippedImage = src;
}
src = this.clippedSvg;
}
return src;
};
// Override to use key as fallback
(function()
{
var mxResourcesGet = mxResources.get;
mxResources.get = function(key, params, defaultValue)
{
if (defaultValue == null)
{
defaultValue = key;
}
return mxResourcesGet.apply(this, [key, params, defaultValue]);
};
})();
/**
* Constructs a new graph instance. Note that the constructor does not take a
* container because the graph instance is needed for creating the UI, which
* in turn will create the container for the graph. Hence, the container is
* assigned later in EditorUi.
*/
/**
* Defines graph class.
*/
Graph = function(container, model, renderHint, stylesheet, themes, standalone)
{
mxGraph.call(this, container, model, renderHint, stylesheet);
this.themes = themes || this.defaultThemes;
this.currentEdgeStyle = mxUtils.clone(this.defaultEdgeStyle);
this.currentVertexStyle = mxUtils.clone(this.defaultVertexStyle);
this.standalone = (standalone != null) ? standalone : false;
// Sets the base domain URL and domain path URL for relative links.
var b = this.baseUrl;
var p = b.indexOf('//');
this.domainUrl = '';
this.domainPathUrl = '';
if (p > 0)
{
var d = b.indexOf('/', p + 2);
if (d > 0)
{
this.domainUrl = b.substring(0, d);
}
d = b.lastIndexOf('/');
if (d > 0)
{
this.domainPathUrl = b.substring(0, d + 1);
}
}
// Adds support for HTML labels via style. Note: Currently, only the Java
// backend supports HTML labels but CSS support is limited to the following:
// http://docs.oracle.com/javase/6/docs/api/index.html?javax/swing/text/html/CSS.html
// TODO: Wrap should not affect isHtmlLabel output (should be handled later)
this.isHtmlLabel = function(cell)
{
var style = this.getCurrentCellStyle(cell);
return (style != null) ? (style['html'] == '1' || style[mxConstants.STYLE_WHITE_SPACE] == 'wrap') : false;
};
// Implements a listener for hover and click handling on edges and tables
if (this.immediateHandling)
{
var start = {
point: null,
event: null,
state: null,
handle: null,
selected: false
};
var initialSelected = false;
// Uses this event to process mouseDown to check the selection state before it is changed
this.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
{
if (evt.getProperty('eventName') == 'mouseDown' && this.isEnabled())
{
var me = evt.getProperty('event');
var state = me.getState();
var s = this.view.scale;
if (!mxEvent.isAltDown(me.getEvent()) && state != null)
{
initialSelected = this.isCellSelected(state.cell);
if (!this.panningHandler.isActive() && !mxEvent.isControlDown(me.getEvent()))
{
var handler = this.selectionCellsHandler.getHandler(state.cell);
// Cell handles have precedence over row and col resize
if (handler == null || handler.getHandleForEvent(me) == null)
{
var box = new mxRectangle(me.getGraphX() - 1, me.getGraphY() - 1);
var tol = mxEvent.isTouchEvent(me.getEvent()) ?
mxShape.prototype.svgStrokeTolerance - 1 :
(mxShape.prototype.svgStrokeTolerance + 2) / 2;
var t1 = tol + 2;
box.grow(tol);
// Ignores clicks inside cell to avoid delayed selection on
// merged cells when clicking on invisible part of dividers
if (this.isTableCell(state.cell) && this.isCellMovable(state.cell) &&
!this.isCellSelected(state.cell) &&
(!mxUtils.contains(state, me.getGraphX() - t1, me.getGraphY() - t1) ||
!mxUtils.contains(state, me.getGraphX() - t1, me.getGraphY() + t1) ||
!mxUtils.contains(state, me.getGraphX() + t1, me.getGraphY() + t1) ||
!mxUtils.contains(state, me.getGraphX() + t1, me.getGraphY() - t1)))
{
var row = this.model.getParent(state.cell);
var table = this.model.getParent(row);
if (!this.isCellSelected(table))
{
var b = tol * s;
var b2 = 2 * b;
// Ignores events on top line of top row and left line of left column
if ((this.model.getChildAt(table, 0) != row) && mxUtils.intersects(box,
new mxRectangle(state.x, state.y - b, state.width, b2)) ||
(this.model.getChildAt(row, 0) != state.cell) && mxUtils.intersects(box,
new mxRectangle(state.x - b, state.y, b2, state.height)) ||
mxUtils.intersects(box, new mxRectangle(state.x, state.y + state.height - b, state.width, b2)) ||
mxUtils.intersects(box, new mxRectangle(state.x + state.width - b, state.y, b2, state.height)))
{
var wasSelected = this.selectionCellsHandler.isHandled(table);
this.selectCellForEvent(table, me.getEvent());
handler = this.selectionCellsHandler.getHandler(table);
if (handler != null)
{
var handle = handler.getHandleForEvent(me);
if (handle != null)
{
handler.start(me.getGraphX(), me.getGraphY(), handle);
handler.blockDelayedSelection = !wasSelected;
me.consume();
}
}
}
}
}
// Hover for swimlane start sizes inside tables
var current = state;
while (!me.isConsumed() && current != null && (this.isTableCell(current.cell) ||
this.isTableRow(current.cell) || this.isTable(current.cell)))
{
if (this.isSwimlane(current.cell) && this.isCellMovable(current.cell))
{
var offset = this.getActualStartSize(current.cell);
if (((offset.x > 0 || offset.width > 0) && mxUtils.intersects(box, new mxRectangle(
current.x + (offset.x - offset.width - 1) * s + ((offset.x == 0) ? current.width : 0),
current.y, 1, current.height))) || ((offset.y > 0 || offset.height > 0) &&
mxUtils.intersects(box, new mxRectangle(current.x, current.y + (offset.y -
offset.height - 1) * s + ((offset.y == 0) ? current.height : 0), current.width, 1))))
{
this.selectCellForEvent(current.cell, me.getEvent());
handler = this.selectionCellsHandler.getHandler(current.cell);
if (handler != null && handler.customHandles != null)
{
// Swimlane start size handle is last custom handle
var handle = mxEvent.CUSTOM_HANDLE - handler.customHandles.length + 1;
handler.start(me.getGraphX(), me.getGraphY(), handle);
me.consume();
}
}
}
current = this.view.getState(this.model.getParent(current.cell));
}
}
}
}
}
}));
// Uses this event to process mouseDown to check the selection state before it is changed
this.addListener(mxEvent.CONSUME_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
{
if (evt.getProperty('eventName') == 'mouseDown' && this.isEnabled())
{
var me = evt.getProperty('event');
var state = me.getState();
if (!mxEvent.isAltDown(me.getEvent()) && !mxEvent.isControlDown(evt) &&
!mxEvent.isShiftDown(evt) && !initialSelected &&
state != null && this.model.isEdge(state.cell))
{
start.point = new mxPoint(me.getGraphX(), me.getGraphY());
start.selected = this.isCellSelected(state.cell);
start.state = state;
start.event = me;
if (state.text != null && state.text.boundingBox != null &&
mxUtils.contains(state.text.boundingBox, me.getGraphX(), me.getGraphY()))
{
start.handle = mxEvent.LABEL_HANDLE;
}
else
{
var handler = this.selectionCellsHandler.getHandler(state.cell);
if (handler != null && handler.bends != null && handler.bends.length > 0)
{
start.handle = handler.getHandleForEvent(me);
}
}
}
}
}));
this.addMouseListener(
{
mouseDown: function(sender, me) {},
mouseMove: mxUtils.bind(this, function(sender, me)
{
// Checks if any other handler is active
var handlerMap = this.selectionCellsHandler.handlers.map;
for (var key in handlerMap)
{
if (handlerMap[key].index != null)
{
return;
}
}
if (this.isEnabled() && !this.panningHandler.isActive() && !mxEvent.isAltDown(me.getEvent()))
{
var tol = this.tolerance;
if (start.point != null && start.state != null && start.event != null)
{
var state = start.state;
if (start.handle != null || Math.abs(start.point.x - me.getGraphX()) > tol ||
Math.abs(start.point.y - me.getGraphY()) > tol)
{
var handler = null;
if (!mxEvent.isControlDown(me.getEvent()) &&
!mxEvent.isShiftDown(me.getEvent()))
{
handler = this.selectionCellsHandler.getHandler(state.cell);
}
if (handler != null && handler.bends != null && handler.bends.length > 0)
{
handler.redrawHandles();
var handle = (start.handle != null) ? start.handle :
handler.getHandleForEvent(start.event);
var edgeStyle = this.view.getEdgeStyle(state);
var entity = edgeStyle == mxEdgeStyle.EntityRelation;
var pts = state.absolutePoints;
// Handles special case where label was clicked on unselected edge in which
// case the label will be moved regardless of the handle that is returned
if (!start.selected)// && start.handle == mxEvent.LABEL_HANDLE)
{
handle = start.handle;
}
if (handle != mxEvent.LABEL_HANDLE && pts != null)
{
// Does not use handles if they were not initially visible
handle = start.handle;
if (handle == null)
{
var box = new mxRectangle(start.point.x, start.point.y);
box.grow(mxEdgeHandler.prototype.handleImage.width / 2);
if (mxUtils.contains(box, pts[0].x, pts[0].y))
{
// Moves source terminal handle
handle = 0;
}
else if (mxUtils.contains(box, pts[pts.length - 1].x, pts[pts.length - 1].y))
{
// Moves target terminal handle
handle = handler.bends.length - 1;
}
else if (pts != null && ((edgeStyle == null && handle == null) ||
edgeStyle == mxEdgeStyle.SegmentConnector ||
edgeStyle == mxEdgeStyle.OrthConnector))
{
// Checks if edge has no bends
var nobends = edgeStyle != null && (pts.length == 2 || (pts.length == 3 &&
((Math.round(pts[0].x - pts[1].x) == 0 && Math.round(pts[1].x - pts[2].x) == 0) ||
(Math.round(pts[0].y - pts[1].y) == 0 && Math.round(pts[1].y - pts[2].y) == 0))));
if (nobends)
{
// Moves central handle for straight orthogonal edges
handle = 2;
}
else
{
// Finds and moves vertical or horizontal segment
handle = mxUtils.findNearestSegment(state, start.point.x, start.point.y);
// Converts segment to virtual handle index
if (edgeStyle == null)
{
handle = mxEvent.VIRTUAL_HANDLE - handle;
}
// Maps segment to handle
else
{
handle += 1;
}
}
}
}
// Creates a new waypoint and starts moving it
if (handle == null)
{
handle = mxEvent.VIRTUAL_HANDLE;
}
}
var validEdge = !entity && (state.visibleSourceState != null ||
state.visibleTargetState != null);
var validHandle = handle == mxEvent.LABEL_HANDLE ||
handle == 0 || handle == handler.bends.length - 1;
if (validEdge || validHandle)
{
handler.start(me.getGraphX(), me.getGraphX(), handle);
me.consume();
// Removes preview rectangle in graph handler
this.graphHandler.reset();
}
}
if (handler != null)
{
// Lazy selection for edges inside groups
if (this.selectionCellsHandler.isHandlerActive(handler))
{
if (!this.isCellSelected(state.cell))
{
this.selectionCellsHandler.handlers.put(state.cell, handler);
this.selectCellForEvent(state.cell, me.getEvent());
}
}
else if (!this.isCellSelected(state.cell))
{
// Destroy temporary handler
handler.destroy();
}
}
// Reset start state
start.selected = false;
start.handle = null;
start.state = null;
start.event = null;
start.point = null;
}
}
else
{
// Updates cursor for unselected edges under the mouse
var state = me.getState();
if (state != null && this.isCellEditable(state.cell))
{
var parent = this.model.getParent(state.cell);
var cursor = null;
// Checks if state was removed in call to stopEditing above
if (this.model.isEdge(state.cell) &&
!this.isCellSelected(state.cell) &&
!mxEvent.isAltDown(me.getEvent()) &&
!mxEvent.isControlDown(me.getEvent()) &&
!mxEvent.isShiftDown(me.getEvent()) &&
// Immediate edge handling unavailable
// in groups and selected ancestors
!this.isAncestorSelected(state.cell) &&
(this.isSwimlane(parent) ||
this.model.isLayer(parent) ||
this.getCurrentRoot() == parent))
{
var box = new mxRectangle(me.getGraphX(), me.getGraphY());
box.grow(mxEdgeHandler.prototype.handleImage.width / 2);
var pts = state.absolutePoints;
if (pts != null)
{
if (state.text != null && state.text.boundingBox != null &&
mxUtils.contains(state.text.boundingBox, me.getGraphX(), me.getGraphY()))
{
cursor = 'move';
}
else if (mxUtils.contains(box, pts[0].x, pts[0].y) ||
mxUtils.contains(box, pts[pts.length - 1].x, pts[pts.length - 1].y))
{
cursor = 'pointer';
}
else
{
var edgeStyle = this.view.getEdgeStyle(state);
if (edgeStyle != mxEdgeStyle.EntityRelation &&
(state.visibleSourceState != null ||
state.visibleTargetState != null))
{
cursor = 'crosshair';
if (edgeStyle == mxEdgeStyle.SegmentConnector ||
edgeStyle == mxEdgeStyle.OrthConnector)
{
var idx = mxUtils.findNearestSegment(state, me.getGraphX(), me.getGraphY());
if (idx < pts.length - 1 && idx >= 0)
{
cursor = (Math.round(pts[idx].x - pts[idx + 1].x) == 0) ?
'col-resize' : 'row-resize';
}
}
}
}
}
}
else if (!mxEvent.isControlDown(me.getEvent()))
{
var tol = mxShape.prototype.svgStrokeTolerance / 2;
var box = new mxRectangle(me.getGraphX(), me.getGraphY());
box.grow(tol);
if (this.isTableCell(state.cell) && this.isCellMovable(state.cell))
{
var row = this.model.getParent(state.cell);
var table = this.model.getParent(row);
if (!this.isCellSelected(table))
{
if ((mxUtils.intersects(box, new mxRectangle(state.x, state.y - 2, state.width, 4)) &&
this.model.getChildAt(table, 0) != row) || mxUtils.intersects(box,
new mxRectangle(state.x, state.y + state.height - 2, state.width, 4)))
{
cursor ='row-resize';
}
else if ((mxUtils.intersects(box, new mxRectangle(state.x - 2, state.y, 4, state.height)) &&
this.model.getChildAt(row, 0) != state.cell) || mxUtils.intersects(box,
new mxRectangle(state.x + state.width - 2, state.y, 4, state.height)))
{
cursor ='col-resize';
}
}
}
// Hover for swimlane start sizes inside tables
var current = state;
while (cursor == null && current != null && (this.isTableCell(current.cell) ||
this.isTableRow(current.cell) || this.isTable(current.cell)))
{
if (this.isSwimlane(current.cell) && this.isCellMovable(current.cell))
{
var offset = this.getActualStartSize(current.cell);
var s = this.view.scale;
if ((offset.x > 0 || offset.width > 0) && mxUtils.intersects(box, new mxRectangle(
current.x + (offset.x - offset.width - 1) * s + ((offset.x == 0) ? current.width * s : 0),
current.y, 1, current.height)))
{
cursor ='col-resize';
}
else if ((offset.y > 0 || offset.height > 0) && mxUtils.intersects(box, new mxRectangle(
current.x, current.y + (offset.y - offset.height - 1) * s + ((offset.y == 0) ? current.height : 0),
current.width, 1)))
{
cursor ='row-resize';
}
}
current = this.view.getState(this.model.getParent(current.cell));
}
}
if (cursor != null)
{
state.setCursor(cursor);
}
}
}
}
}),
mouseUp: mxUtils.bind(this, function(sender, me)
{
start.state = null;
start.event = null;
start.point = null;
start.handle = null;
})
});
}
this.cellRenderer.minSvgStrokeWidth = 0.1;
// HTML entities are displayed as plain text in wrapped plain text labels
this.cellRenderer.getLabelValue = function(state)
{
var result = mxCellRenderer.prototype.getLabelValue.apply(this, arguments);
if (state.view.graph.isHtmlLabel(state.cell))
{
if (state.style['html'] != 1)
{
result = mxUtils.htmlEntities(result, false);
}
else
{
// Skips sanitizeHtml for unchanged labels
if (state.lastLabelValue != result)
{
state.lastLabelValue = result;
state.lastSanitizedLabelValue = Graph.sanitizeHtml(result);
}
result = state.lastSanitizedLabelValue;
}
}
return result;
};
// All code below not available and not needed in embed mode
if (typeof mxVertexHandler !== 'undefined')
{
this.setConnectable(true);
this.setDropEnabled(true);
this.setPanning(true);
this.setTooltips(true);
this.setAllowLoops(true);
this.allowAutoPanning = true;
this.resetEdgesOnConnect = false;
this.constrainChildren = false;
this.constrainRelativeChildren = true;
// Do not scroll after moving cells
this.graphHandler.scrollOnMove = false;
this.graphHandler.scaleGrid = true;
// Disables cloning of connection sources by default
this.connectionHandler.setCreateTarget(false);
this.connectionHandler.insertBeforeSource = true;
// Disables built-in connection starts
this.connectionHandler.isValidSource = function(cell, me)
{
return false;
};
// Sets the style to be used when an elbow edge is double clicked
this.alternateEdgeStyle = 'vertical';
if (stylesheet == null)
{
this.loadStylesheet();
}
// Adds page centers to the guides for moving cells
var graphHandlerGetGuideStates = this.graphHandler.getGuideStates;
this.graphHandler.getGuideStates = function()
{
var result = graphHandlerGetGuideStates.apply(this, arguments);
// Create virtual cell state for page centers
if (this.graph.pageVisible)
{
var guides = [];
var pf = this.graph.pageFormat;
var ps = this.graph.pageScale;
var pw = pf.width * ps;
var ph = pf.height * ps;
var t = this.graph.view.translate;
var s = this.graph.view.scale;
var layout = this.graph.getPageLayout();
for (var i = 0; i < layout.width; i++)
{
guides.push(new mxRectangle(((layout.x + i) * pw + t.x) * s,
(layout.y * ph + t.y) * s, pw * s, ph * s));
}
for (var j = 1; j < layout.height; j++)
{
guides.push(new mxRectangle((layout.x * pw + t.x) * s,
((layout.y + j) * ph + t.y) * s, pw * s, ph * s));
}
// Page center guides have precedence over normal guides
result = guides.concat(result);
}
return result;
};
// Overrides zIndex for dragElement
mxDragSource.prototype.dragElementZIndex = mxPopupMenu.prototype.zIndex;
// Overrides color for virtual guides for page centers
mxGuide.prototype.getGuideColor = function(state, horizontal)
{
return (state.cell == null) ? '#ffa500' /* orange */ : mxConstants.GUIDE_COLOR;
};
// Changes color of move preview for black backgrounds
this.graphHandler.createPreviewShape = function(bounds)
{
this.previewColor = (this.graph.background == '#000000') ? '#ffffff' : mxGraphHandler.prototype.previewColor;
return mxGraphHandler.prototype.createPreviewShape.apply(this, arguments);
};
// Handles parts of cells by checking if part=1 is in the style and returning the parent
// if the parent is not already in the list of cells. container style is used to disable
// step into swimlanes and dropTarget style is used to disable acting as a drop target.
// LATER: Handle recursive parts
var graphHandlerGetCells = this.graphHandler.getCells;
this.graphHandler.getCells = function(initialCell)
{
var cells = graphHandlerGetCells.apply(this, arguments);
var lookup = new mxDictionary();
var newCells = [];
for (var i = 0; i < cells.length; i++)
{
// Propagates to composite parents or moves selected table rows
var cell = (this.graph.isTableCell(initialCell) &&
this.graph.isTableCell(cells[i]) &&
this.graph.isCellSelected(cells[i])) ?
this.graph.model.getParent(cells[i]) :
((this.graph.isTableRow(initialCell) &&
this.graph.isTableRow(cells[i]) &&
this.graph.isCellSelected(cells[i])) ?
cells[i] : this.graph.getCompositeParent(cells[i]));
if (cell != null && !lookup.get(cell))
{
lookup.put(cell, true);
newCells.push(cell);
}
}
return newCells;
};
// Handles parts and selected rows in tables of cells for drag and drop
var graphHandlerStart = this.graphHandler.start;
this.graphHandler.start = function(cell, x, y, cells)
{
// Propagates to selected table row to start move
var ignoreParent = false;
if (this.graph.isTableCell(cell))
{
if (!this.graph.isCellSelected(cell))
{
cell = this.graph.model.getParent(cell);
}
else
{
ignoreParent = true;
}
}
if (!ignoreParent && (!this.graph.isTableRow(cell) || !this.graph.isCellSelected(cell)))
{
cell = this.graph.getCompositeParent(cell);
}
graphHandlerStart.apply(this, arguments);
};
// Handles parts of cells when cloning the source for new connections
this.connectionHandler.createTargetVertex = function(evt, source)
{
source = this.graph.getCompositeParent(source);
return mxConnectionHandler.prototype.createTargetVertex.apply(this, arguments);
};
// Applies newEdgeStyle
this.connectionHandler.insertEdge = function(parent, id, value, source, target, style)
{
var edge = mxConnectionHandler.prototype.insertEdge.apply(this, arguments);
if (source != null)
{
this.graph.applyNewEdgeStyle(source, [edge]);
}
return edge
};
// Creates rubberband selection and associates with graph instance
var rubberband = new mxRubberband(this);
this.getRubberband = function()
{
return rubberband;
};
// Timer-based activation of outline connect in connection handler
var startTime = new Date().getTime();
var timeOnTarget = 0;
var connectionHandlerMouseMove = this.connectionHandler.mouseMove;
this.connectionHandler.mouseMove = function()
{
var prev = this.currentState;
connectionHandlerMouseMove.apply(this, arguments);
if (prev != this.currentState)
{
startTime = new Date().getTime();
timeOnTarget = 0;
}
else
{
timeOnTarget = new Date().getTime() - startTime;
}
};
// Activates outline connect after 1500ms with touch event or if alt is pressed inside the shape
// outlineConnect=0 is a custom style that means do not connect to strokes inside the shape,
// or in other words, connect to the shape's perimeter if the highlight is under the mouse
// (the name is because the highlight, including all strokes, is called outline in the code)
var connectionHandleIsOutlineConnectEvent = this.connectionHandler.isOutlineConnectEvent;
this.connectionHandler.isOutlineConnectEvent = function(me)
{
if (mxEvent.isShiftDown(me.getEvent()) && mxEvent.isAltDown(me.getEvent()))
{
return false;
}
else
{
return (this.currentState != null && me.getState() == this.currentState && timeOnTarget > 2000) ||
((this.currentState == null || mxUtils.getValue(this.currentState.style, 'outlineConnect', '1') != '0') &&
connectionHandleIsOutlineConnectEvent.apply(this, arguments));
}
};
// Adds shift+click to toggle selection state
var isToggleEvent = this.isToggleEvent;
this.isToggleEvent = function(evt)
{
return isToggleEvent.apply(this, arguments) || (!mxClient.IS_CHROMEOS && mxEvent.isShiftDown(evt));
};
// Workaround for Firefox where first mouse down is received
// after tap and hold if scrollbars are visible, which means
// start rubberband immediately if no cell is under mouse.
var isForceRubberBandEvent = rubberband.isForceRubberbandEvent;
rubberband.isForceRubberbandEvent = function(me)
{
return isForceRubberBandEvent.apply(this, arguments) ||
(mxClient.IS_CHROMEOS && mxEvent.isShiftDown(me.getEvent())) ||
(mxUtils.hasScrollbars(this.graph.container) && mxClient.IS_FF &&
mxClient.IS_WIN && me.getState() == null && mxEvent.isTouchEvent(me.getEvent()));
};
// Shows hand cursor while panning
var prevCursor = null;
this.panningHandler.addListener(mxEvent.PAN_START, mxUtils.bind(this, function()
{
if (this.isEnabled())
{
prevCursor = this.container.style.cursor;
this.container.style.cursor = 'move';
}
}));
this.panningHandler.addListener(mxEvent.PAN_END, mxUtils.bind(this, function()
{
if (this.isEnabled())
{
this.container.style.cursor = prevCursor;
}
}));
this.popupMenuHandler.autoExpand = true;
this.popupMenuHandler.isSelectOnPopup = function(me)
{
return mxEvent.isMouseEvent(me.getEvent());
};
// Handles links in read-only graphs
// and cells in locked layers
var click = this.click;
this.click = function(me)
{
var locked = me.state == null && me.sourceState != null &&
this.isCellLocked(this.getLayerForCell(
me.sourceState.cell));
if ((!this.isEnabled() || locked) && !me.isConsumed())
{
var cell = (locked) ? me.sourceState.cell : me.getCell();
if (cell != null)
{
var link = this.getClickableLinkForCell(cell);
if (link != null)
{
if (this.isCustomLink(link))
{
this.customLinkClicked(link, cell);
}
else
{
this.openLink(link);
}
}
else if (locked)
{
this.clearSelection();
}
}
}
else
{
return click.apply(this, arguments);
}
};
// Redirects tooltips for locked cells
this.tooltipHandler.getStateForEvent = function(me)
{
return me.sourceState;
};
// Opens links in tooltips in new windows
var tooltipHandlerShow = this.tooltipHandler.show;
this.tooltipHandler.show = function()
{
tooltipHandlerShow.apply(this, arguments);
if (this.div != null)
{
var links = this.div.getElementsByTagName('a');
for (var i = 0; i < links.length; i++)
{
if (links[i].getAttribute('href') != null &&
links[i].getAttribute('target') == null)
{
links[i].setAttribute('target', '_blank');
}
}
}
};
// Redirects tooltips for locked cells
this.tooltipHandler.getStateForEvent = function(me)
{
return me.sourceState;
};
// Redirects cursor for locked cells
var getCursorForMouseEvent = this.getCursorForMouseEvent;
this.getCursorForMouseEvent = function(me)
{
var locked = me.state == null && me.sourceState != null && this.isCellLocked(me.sourceState.cell);
return this.getCursorForCell((locked) ? me.sourceState.cell : me.getCell());
};
// Shows pointer cursor for clickable cells with links
// ie. if the graph is disabled and cells cannot be selected
var getCursorForCell = this.getCursorForCell;
this.getCursorForCell = function(cell)
{
if (!this.isEnabled() || this.isCellLocked(cell))
{
var link = this.getClickableLinkForCell(cell);
if (link != null)
{
return 'pointer';
}
else if (this.isCellLocked(cell))
{
return 'default';
}
}
return getCursorForCell.apply(this, arguments);
};
// Changes rubberband selection ignore locked cells
this.selectRegion = function(rect, evt)
{
var isect = (mxEvent.isAltDown(evt)) ? rect : null;
var cells = this.getCells(rect.x, rect.y,
rect.width, rect.height, null, null,
isect, null, true);
if (this.isToggleEvent(evt))
{
for (var i = 0; i < cells.length; i++)
{
this.selectCellForEvent(cells[i], evt);
}
}
else
{
this.selectCellsForEvent(cells, evt);
}
return cells;
};
// Never removes cells from parents that are being moved
var graphHandlerShouldRemoveCellsFromParent = this.graphHandler.shouldRemoveCellsFromParent;
this.graphHandler.shouldRemoveCellsFromParent = function(parent, cells, evt)
{
if (this.graph.isCellSelected(parent))
{
return false;
}
return graphHandlerShouldRemoveCellsFromParent.apply(this, arguments);
};
// Enables rubberband selection on cells in locked layers
var graphUpdateMouseEvent = this.updateMouseEvent;
this.updateMouseEvent = function(me)
{
me = graphUpdateMouseEvent.apply(this, arguments);
if (me.state != null && this.isCellLocked(this.getLayerForCell(me.getCell())))
{
if (this.getLinkForCell(me.getCell()) == null)
{
me.state = this.view.getState(this.getCellAt(me.getGraphX(), me.getGraphY(),
null, null, null, mxUtils.bind(this, function(state, x, y)
{
return me.state == state || (this.isCellLocked(this.getLayerForCell(state.cell)) &&
this.getLinkForCell(state.cell) == null);
})));
}
else
{
me.state = null;
}
}
return me;
};
// Cells in locked layers are not selectable
var graphIsCellSelectable = this.isCellSelectable;
this.isCellSelectable = function(cell)
{
return graphIsCellSelectable.apply(this, arguments) &&
!this.isCellLocked(this.getLayerForCell(cell));
};
// Returns true if the given cell is locked
this.isCellLocked = function(cell)
{
while (cell != null)
{
if (mxUtils.getValue(this.getCurrentCellStyle(cell), 'locked', '0') == '1')
{
return true;
}
cell = this.model.getParent(cell);
}
return false;
};
var tapAndHoldSelection = null;
// Uses this event to process mouseDown to check the selection state before it is changed
this.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
{
if (evt.getProperty('eventName') == 'mouseDown')
{
var me = evt.getProperty('event');
var state = me.getState();
if (state != null && !this.isSelectionEmpty() && !this.isCellSelected(state.cell))
{
tapAndHoldSelection = this.getSelectionCells();
}
else
{
tapAndHoldSelection = null;
}
}
}));
// Tap and hold on background starts rubberband for multiple selected
// cells the cell associated with the event is deselected
this.addListener(mxEvent.TAP_AND_HOLD, mxUtils.bind(this, function(sender, evt)
{
if (!mxEvent.isMultiTouchEvent(evt))
{
var me = evt.getProperty('event');
var cell = evt.getProperty('cell');
if (cell == null)
{
var pt = mxUtils.convertPoint(this.container,
mxEvent.getClientX(me), mxEvent.getClientY(me));
rubberband.start(pt.x, pt.y);
}
else if (tapAndHoldSelection != null)
{
this.addSelectionCells(tapAndHoldSelection);
}
else if (this.getSelectionCount() > 1 && this.isCellSelected(cell))
{
this.removeSelectionCell(cell);
}
// Blocks further processing of the event
tapAndHoldSelection = null;
evt.consume();
}
}));
// On connect the target is selected and we clone the cell of the preview edge for insert
this.connectionHandler.selectCells = function(edge, target)
{
this.graph.setSelectionCell(target || edge);
};
// Shows connection points only if cell not selected and parent table not handled
this.connectionHandler.constraintHandler.isStateIgnored = function(state, source)
{
var graph = state.view.graph;
return source && (graph.isCellSelected(state.cell) || (graph.isTableRow(state.cell) &&
graph.selectionCellsHandler.isHandled(graph.model.getParent(state.cell))));
};
// Updates constraint handler if the selection changes
this.selectionModel.addListener(mxEvent.CHANGE, mxUtils.bind(this, function()
{
var ch = this.connectionHandler.constraintHandler;
if (ch.currentFocus != null && ch.isStateIgnored(ch.currentFocus, true))
{
ch.currentFocus = null;
ch.constraints = null;
ch.destroyIcons();
}
ch.destroyFocusHighlight();
}));
// Initializes touch interface
if (Graph.touchStyle)
{
this.initTouch();
}
}
//Create a unique offset object for each graph instance.
this.currentTranslate = new mxPoint(0, 0);
};
/**
* Specifies if the touch UI should be used (cannot detect touch in FF so always on for Windows/Linux)
*/
Graph.touchStyle = mxClient.IS_TOUCH || (mxClient.IS_FF && mxClient.IS_WIN) || navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0 || window.urlParams == null || urlParams['touch'] == '1';
/**
* Shortcut for capability check.
*/
Graph.fileSupport = window.File != null && window.FileReader != null && window.FileList != null &&
(window.urlParams == null || urlParams['filesupport'] != '0');
/**
* Shortcut for capability check.
*/
Graph.translateDiagram = urlParams['translate-diagram'] == '1';
/**
* Shortcut for capability check.
*/
Graph.diagramLanguage = (urlParams['diagram-language'] != null) ? urlParams['diagram-language'] : mxClient.language;
/**
* Default size for line jumps.
*/
Graph.lineJumpsEnabled = true;
/**
* Default size for line jumps.
*/
Graph.defaultJumpSize = 6;
/**
* Specifies if the mouse wheel is used for zoom without any modifiers.
*/
Graph.zoomWheel = false;
/**
* Specifies if the parent layer should be selected when the selection changes.
* Default is false.
*/
Graph.selectParentLayer = false;
/**
* Minimum width for table columns.
*/
Graph.minTableColumnWidth = 20;
/**
* Minimum height for table rows.
*/
Graph.minTableRowHeight = 20;
/**
* Text for foreign object warning.
*/
Graph.foreignObjectWarningText = 'Text is not SVG - cannot display';
/**
* Link for foreign object warning.
*/
Graph.foreignObjectWarningLink = 'https://www.drawio.com/doc/faq/svg-export-text-problems';
/**
*
*/
Graph.xmlDeclaration = '';
/**
*
*/
Graph.svgDoctype = '';
/**
*
*/
Graph.svgFileComment = ''
/**
* Minimum height for table rows.
*/
Graph.pasteStyles = ['rounded', 'shadow', 'dashed', 'dashPattern', 'fontFamily', 'fontSource', 'fontSize', 'fontColor', 'fontStyle',
'align', 'verticalAlign', 'strokeColor', 'strokeWidth', 'fillColor', 'gradientColor', 'swimlaneFillColor',
'textOpacity', 'gradientDirection', 'glass', 'labelBackgroundColor', 'labelBorderColor', 'opacity',
'spacing', 'spacingTop', 'spacingLeft', 'spacingBottom', 'spacingRight', 'endFill', 'endArrow',
'endSize', 'targetPerimeterSpacing', 'startFill', 'startArrow', 'startSize', 'sourcePerimeterSpacing',
'arcSize', 'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle', 'disableMultiStroke',
'disableMultiStrokeFill', 'fillStyle', 'curveFitting', 'simplification', 'comicStyle'];
/**
* Whitelist for known layout names.
*/
Graph.layoutNames = ['mxHierarchicalLayout', 'mxCircleLayout', 'mxCompactTreeLayout',
'mxEdgeLabelLayout', 'mxFastOrganicLayout', 'mxParallelEdgeLayout',
'mxPartitionLayout', 'mxRadialTreeLayout', 'mxStackLayout'];
/**
* Creates a temporary graph instance for rendering off-screen content.
*/
Graph.createOffscreenGraph = function(stylesheet)
{
var graph = new Graph(document.createElement('div'));
graph.stylesheet.styles = mxUtils.clone(stylesheet.styles);
graph.resetViewOnRootChange = false;
graph.setConnectable(false);
graph.gridEnabled = false;
graph.autoScroll = false;
graph.setTooltips(false);
graph.setEnabled(false);
// Container must be in the DOM for correct HTML rendering
graph.container.style.visibility = 'hidden';
graph.container.style.position = 'absolute';
graph.container.style.overflow = 'hidden';
graph.container.style.height = '1px';
graph.container.style.width = '1px';
return graph;
};
/**
* Helper function for creating SVG data URI.
*/
Graph.createSvgImage = function(w, h, data, coordWidth, coordHeight)
{
var tmp = unescape(encodeURIComponent(Graph.svgDoctype +
''));
return new mxImage('data:image/svg+xml;base64,' + ((window.btoa) ? btoa(tmp) : Base64.encode(tmp, true)), w, h)
};
/**
*
*/
Graph.createSvgDarkModeCss = function(cssClass)
{
cssClass = (cssClass != null) ? '.' + cssClass : '';
return 'svg' + cssClass + ' > * { filter: invert(100%) hue-rotate(180deg); }\n' +
'svg' + cssClass + ' image { filter: invert(100%) hue-rotate(180deg) }';
};
/**
*
*/
Graph.createSvgDarkModeStyle = function(svgDoc, theme, cssClass)
{
var style = (svgDoc.createElementNS != null) ?
svgDoc.createElementNS(mxConstants.NS_SVG, 'style') : svgDoc.createElement('style');
svgDoc.setAttributeNS != null? style.setAttributeNS('type', 'text/css') :
style.setAttribute('type', 'text/css');
var css = Graph.createSvgDarkModeCss(cssClass);
if (theme == 'auto')
{
cssClass = (cssClass != null) ? '.' + cssClass : '';
css = '@media (prefers-color-scheme: dark) {' + css + '\n' +
' svg' + cssClass + '[style^="background-color: rgb(255, 255, 255);"] {' +
' background-color: ' + Editor.darkColor + ' !important;' +
' }}';
}
style.appendChild(svgDoc.createTextNode(css));
return style;
};
/**
*
*/
Graph.getSvgFromDataUri = function(uri)
{
if (uri != null && uri.substring(0, 14) == 'data:image/svg')
{
return Graph.xmlDeclaration + '\n' + Graph.svgDoctype + '\n' +
decodeURIComponent(escape(atob(uri.substring(
uri.indexOf(',') + 1))));
}
else
{
return null;
}
};
/**
* Helper function for creating an SVG node.
*/
Graph.createSvgNode = function(x, y, w, h, background)
{
var svgDoc = mxUtils.createXmlDocument();
var root = (svgDoc.createElementNS != null) ?
svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') :
svgDoc.createElement('svg');
if (background != null)
{
if (root.style != null)
{
root.style.backgroundColor = background;
}
else
{
root.setAttribute('style', 'background-color:' + background);
}
}
if (svgDoc.createElementNS == null)
{
root.setAttribute('xmlns', mxConstants.NS_SVG);
root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
}
else
{
// KNOWN: Ignored in IE9-11, adds namespace for each image element instead. No workaround.
root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
}
root.setAttribute('version', '1.1');
root.setAttribute('width', w + 'px');
root.setAttribute('height', h + 'px');
root.setAttribute('viewBox', x + ' ' + y + ' ' + w + ' ' + h);
svgDoc.appendChild(root);
return root;
};
/**
* Helper function for creating an SVG node.
*/
Graph.htmlToPng = function(html, w, h, fn)
{
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var img = document.createElement('img');
img.onload = mxUtils.bind(this, function()
{
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0)
fn(canvas.toDataURL());
});
img.src = 'data:image/svg+xml,' + encodeURIComponent('');
};
/**
* Removes all illegal control characters with ASCII code <32 except TAB, LF
* and CR.
*/
Graph.zapGremlins = function(text)
{
var lastIndex = 0;
var checked = [];
for (var i = 0; i < text.length; i++)
{
var code = text.charCodeAt(i);
// Removes all control chars except TAB, LF and CR
if (!((code >= 32 || code == 9 || code == 10 || code == 13) &&
code != 0xFFFF && code != 0xFFFE))
{
checked.push(text.substring(lastIndex, i));
lastIndex = i + 1;
}
}
if (lastIndex > 0 && lastIndex < text.length)
{
checked.push(text.substring(lastIndex));
}
return (checked.length == 0) ? text : checked.join('');
};
/**
* Turns the given string into an array.
*/
Graph.stringToBytes = function(str)
{
var arr = new Array(str.length);
for (var i = 0; i < str.length; i++)
{
arr[i] = str.charCodeAt(i);
}
return arr;
};
/**
* Turns the given array into a string.
*/
Graph.bytesToString = function(arr)
{
var result = new Array(arr.length);
for (var i = 0; i < arr.length; i++)
{
result[i] = String.fromCharCode(arr[i]);
}
return result.join('');
};
/**
* Turns the given array into a string.
*/
Graph.base64EncodeUnicode = function(str)
{
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}));
};
/**
* Turns the given array into a string.
*/
Graph.base64DecodeUnicode = function(str)
{
return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''));
};
/**
* Returns a base64 encoded version of the compressed outer XML of the given node.
*/
Graph.compressNode = function(node, checked)
{
var xml = mxUtils.getXml(node);
return Graph.compress((checked) ? xml : Graph.zapGremlins(xml));
};
/**
* Returns a string for the given array buffer.
*/
Graph.arrayBufferToString = function(buffer)
{
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++)
{
binary += String.fromCharCode(bytes[i]);
}
return binary;
};
/**
* Returns an array buffer for the given string.
*/
Graph.stringToArrayBuffer = function(data)
{
return Uint8Array.from(data, function (c)
{
return c.charCodeAt(0);
});
};
/**
* Returns index of a string in an array buffer (UInt8Array)
*/
Graph.arrayBufferIndexOfString = function (uint8Array, str, start)
{
var c0 = str.charCodeAt(0), j = 1, p = -1;
//Index of first char
for (var i = start || 0; i < uint8Array.byteLength; i++)
{
if (uint8Array[i] == c0)
{
p = i;
break;
}
}
for (var i = p + 1; p > -1 && i < uint8Array.byteLength && i < p + str.length - 1; i++)
{
if (uint8Array[i] != str.charCodeAt(j))
{
return Graph.arrayBufferIndexOfString(uint8Array, str, p + 1);
}
j++;
}
return j == str.length - 1? p : -1;
};
/**
* Returns a base64 encoded version of the compressed string.
*/
Graph.compress = function(data, deflate)
{
if (data == null || data.length == 0 || typeof(pako) === 'undefined')
{
return data;
}
else
{
var tmp = (deflate) ? pako.deflate(encodeURIComponent(data)) :
pako.deflateRaw(encodeURIComponent(data));
return btoa(Graph.arrayBufferToString(new Uint8Array(tmp)));
}
};
/**
* Returns a decompressed version of the base64 encoded string.
*/
Graph.decompress = function(data, inflate, checked)
{
if (data == null || data.length == 0 || typeof(pako) === 'undefined')
{
return data;
}
else
{
var tmp = Graph.stringToArrayBuffer(atob(data));
var inflated = decodeURIComponent((inflate) ?
pako.inflate(tmp, {to: 'string'}) :
pako.inflateRaw(tmp, {to: 'string'}));
return (checked) ? inflated : Graph.zapGremlins(inflated);
}
};
/**
* Fades the given nodes in or out.
*/
Graph.fadeNodes = function(nodes, start, end, done, delay)
{
delay = (delay != null) ? delay : 1000;
Graph.setTransitionForNodes(nodes, null);
Graph.setOpacityForNodes(nodes, start);
window.setTimeout(function()
{
Graph.setTransitionForNodes(nodes,
'all ' + delay + 'ms ease-in-out');
Graph.setOpacityForNodes(nodes, end);
window.setTimeout(function()
{
Graph.setTransitionForNodes(nodes, null);
if (done != null)
{
done();
}
}, delay);
}, 0);
};
/**
* Adds the label menu items to the given menu and parent.
*/
Graph.exploreFromCell = function(sourceGraph, selectionCell, config)
{
pageSize = (config != null && config.pageSize != null) ? config.pageSize : 8;
minSize = (config != null && config.minSize != null) ? config.minSize : 180;
//
// Main function
//
function exploreFromHere(sourceGraph, selectionCell)
{
var container = document.createElement('div');
container.style.position = 'absolute';
container.style.display = 'block';
container.style.background = (Editor.isDarkMode()) ?
Editor.darkColor : '#ffffff';
container.style.width = '100%';
container.style.height = '100%';
container.style.left = '0px';
container.style.top = '0px';
container.style.zIndex = 2;
var deleteImage = document.createElement('img');
deleteImage.setAttribute('src', Editor.closeBlackImage);
deleteImage.style.position = 'absolute';
deleteImage.style.cursor = 'pointer';
deleteImage.style.right = '10px';
deleteImage.style.top = '10px';
container.appendChild(deleteImage);
var closeLabel = document.createElement('div');
closeLabel.style.position = 'absolute';
closeLabel.style.cursor = 'pointer';
closeLabel.style.right = '38px';
closeLabel.style.top = '14px';
closeLabel.style.textAlign = 'right';
closeLabel.style.verticalAlign = 'top';
mxUtils.write(closeLabel, mxResources.get('close'));
container.appendChild(closeLabel);
document.body.appendChild(container);
var keyHandler = function(evt)
{
if (evt.keyCode == 27)
{
deleteImage.click();
}
};
mxEvent.addListener(document, 'keydown', keyHandler);
function main(container)
{
// Checks if browser is supported
if (!mxClient.isBrowserSupported())
{
// Displays an error message if the browser is
// not supported.
mxUtils.error('Browser is not supported!', 200, false);
}
else
{
// Creates the graph inside the given container
var graph = new Graph(container);
graph.keepEdgesInBackground = true;
graph.isCellResizable = function()
{
return false;
};
// Workaround to hide custom handles
graph.isCellRotatable = function()
{
return false;
};
// Shows hand cursor for all vertices
graph.getCursorForCell = function(cell)
{
if (this.model.isVertex(cell))
{
return 'pointer';
}
return null;
};
graph.getFoldingImage = function()
{
return null;
};
var closeHandler = function()
{
mxEvent.removeListener(document, 'keydown', keyHandler);
container.parentNode.removeChild(container);
// FIXME: Does not work
sourceGraph.scrollCellToVisible(selectionCell);
};
mxEvent.addListener(deleteImage, 'click', closeHandler);
mxEvent.addListener(closeLabel, 'click', closeHandler);
// Disables all built-in interactions
graph.setEnabled(false);
var graphClick = graph.click;
// Handles clicks on cells
graph.click = function(me)
{
var cell = me.getCell();
var realCell = (cell != null) ? ((cell.referenceCell != null) ?
cell.referenceCell : cell) : null;
if (cell != null && graph.rootCell != cell &&
graph.getEdges(realCell).length > 0)
{
load(graph, cell);
}
else
{
graphClick.apply(this, arguments);
}
};
console.log('graph', graph);
var cx = graph.container.scrollWidth / 2;
var cy = graph.container.scrollHeight / 3;
graph.model.beginUpdate();
var cell = graph.importCells([selectionCell])[0];
cell.sourceCellId = selectionCell.id;
cell.geometry.x = cx - cell.geometry.width / 2;
cell.geometry.y = cy - cell.geometry.height / 2;
graph.model.endUpdate();
// Animates the changes in the graph model
graph.getModel().addListener(mxEvent.CHANGE, function(sender, evt)
{
var changes = evt.getProperty('edit').changes;
mxText.prototype.enableBoundingBox = false;
graph.labelsVisible = false;
mxEffects.animateChanges(graph, changes, function()
{
mxText.prototype.enableBoundingBox = true;
graph.labelsVisible = true;
graph.tooltipHandler.hide();
graph.refresh();
});
});
load(graph, cell);
}
};
// Loads the links for the given cell into the given graph
// by requesting the respective data in the server-side
// (implemented for this demo using the server-function)
function load(graph, cell)
{
if (graph.getModel().isVertex(cell))
{
var cx = graph.container.scrollWidth / 2;
var cy = graph.container.scrollHeight / 2;
// Gets the default parent for inserting new cells. This
// is normally the first child of the root (ie. layer 0).
// var parent = graph.getDefaultParent();
graph.rootCell = cell.referenceCell || cell;
// Adds cells to the model in a single step
graph.getModel().beginUpdate();
try
{
var cells = rootChanged(graph, cell);
// Removes all cells except the new root
for (var key in graph.getModel().cells)
{
var tmp = graph.getModel().getCell(key);
if (tmp != graph.rootCell && !graph.getModel().isAncestor(
graph.rootCell, tmp) && graph.getModel().isVertex(tmp))
{
graph.removeCells([tmp]);
}
}
// Merges the response model with the client model
//graph.getModel().mergeChildren(model.getRoot().getChildAt(0), parent);
graph.addCells(cells);
// Moves the given cell to the center
var geo = graph.getModel().getGeometry(graph.rootCell);
if (geo != null)
{
geo = geo.clone();
geo.x = cx - geo.width / 2;
geo.y = cy - geo.height / 3;
graph.getModel().setGeometry(graph.rootCell, geo);
}
// Creates a list of the new vertices, if there is more
// than the center vertex which might have existed
// previously, then this needs to be changed to analyze
// the target model before calling mergeChildren above
var vertices = [];
for (var key in graph.getModel().cells)
{
var tmp = graph.getModel().getCell(key);
if (tmp != graph.rootCell && graph.getModel().isVertex(tmp) &&
graph.getModel().getParent(tmp) == graph.getDefaultParent())
{
vertices.push(tmp);
// Changes the initial location "in-place"
// to get a nice animation effect from the
// center to the radius of the circle
var geo = graph.getModel().getGeometry(tmp);
if (geo != null)
{
geo.x = cx - geo.width / 2;
geo.y = cy - geo.height / 2;
}
}
}
// Arranges the response in a circle
var cellCount = vertices.length;
var phi = 2 * Math.PI / cellCount;
var r = Math.max(minSize, Math.min(graph.container.scrollWidth / 3 - 80,
graph.container.scrollHeight / 3 - 80));
for (var i = 0; i < cellCount; i++)
{
var geo = graph.getModel().getGeometry(vertices[i]);
if (geo != null)
{
geo = geo.clone();
geo.x += r * Math.sin(i * phi);
geo.y += r * Math.cos(i * phi);
graph.getModel().setGeometry(vertices[i], geo);
}
}
// Keeps parallel edges apart
var layout = new mxParallelEdgeLayout(graph);
layout.spacing = 60;
layout.execute(graph.getDefaultParent());
}
finally
{
// Updates the display
graph.getModel().endUpdate();
}
}
};
// Gets the edges from the source cell and adds the targets
function rootChanged(graph, cell)
{
// TODO: Keep existing cells, probably best via XML to redirect IDs
var realCell = cell.referenceCell || cell;
var sourceCell = sourceGraph.model.getCell(realCell.sourceCellId);
var edges = sourceGraph.getEdges(sourceCell, null, true, true, false, true);
// Removes edges with no opposite
var validEdges = [];
for (var i = 0; i < edges.length; i++)
{
if (edges[i].getTerminal(true) != null && edges[i].getTerminal(false) != null)
{
validEdges.push(edges[i]);
}
}
edges = validEdges;
var cells = edges;
// Paging by selecting a window in the edges array
if (cell.startIndex != null || (pageSize > 0 && edges.length > pageSize))
{
var start = cell.startIndex || 0;
cells = edges.slice(Math.max(0, start), Math.min(edges.length, start + pageSize));
}
cells = cells.concat(sourceGraph.getOpposites(cells, sourceCell));
var clones = graph.cloneCells(cells);
var edgeStyle = ';curved=1;noEdgeStyle=1;entryX=none;entryY=none;exitX=none;exitY=none;';
var btnStyle = 'fillColor=green;fontColor=white;strokeColor=green;rounded=1;';
for (var i = 0; i < cells.length; i++)
{
clones[i].sourceCellId = cells[i].id;
if (graph.model.isEdge(clones[i]))
{
// Removes waypoints, edge styles, constraints and centers the label
clones[i].geometry.x = 0;
clones[i].geometry.y = 0;
clones[i].geometry.points = null;
clones[i].setStyle(clones[i].getStyle() + edgeStyle);
clones[i].setTerminal(realCell, clones[i].getTerminal(true) == null);
}
}
if (cell.startIndex > 0)
{
var backCell = graph.createVertex(null, null,
mxResources.get('previousPage') + '...',
0, 0, 100, 30, btnStyle);
backCell.referenceCell = realCell;
backCell.startIndex = Math.max(0, (cell.startIndex || 0) - pageSize);
clones.splice(0, 0, backCell);
}
if (edges.length > (cell.startIndex || 0) + pageSize)
{
var moreCell = graph.createVertex(null, null,
mxResources.get('nextPage') + '...',
0, 0, 100, 30, btnStyle);
moreCell.referenceCell = realCell;
moreCell.startIndex = (cell.startIndex || 0) + pageSize;
clones.splice(0, 0, moreCell);
}
return clones;
};
main(container);
};
exploreFromHere(sourceGraph, selectionCell);
};
/**
* Removes the elements from the map where the given function returns true.
*/
Graph.removeKeys = function(map, ignoreFn)
{
for (var key in map)
{
if (ignoreFn(key))
{
delete map[key];
}
}
};
/**
* Sets the transition for the given nodes.
*/
Graph.setTransitionForNodes = function(nodes, transition)
{
for (var i = 0; i < nodes.length; i++)
{
mxUtils.setPrefixedStyle(nodes[i].style, 'transition', transition);
}
};
/**
* Sets the opacity for the given nodes.
*/
Graph.setOpacityForNodes = function(nodes, opacity)
{
for (var i = 0; i < nodes.length; i++)
{
nodes[i].style.opacity = opacity;
}
};
/**
* Removes formatting from pasted HTML.
*/
Graph.removePasteFormatting = function(elt, ignoreTabs)
{
while (elt != null)
{
if (elt.firstChild != null)
{
Graph.removePasteFormatting(elt.firstChild, true);
}
var next = elt.nextSibling;
if (elt.nodeType == mxConstants.NODETYPE_ELEMENT && elt.style != null)
{
elt.style.whiteSpace = '';
if (elt.style.color == '#000000')
{
elt.style.color = '';
}
// Replaces tabs from macOS TextEdit
if (elt.nodeName == 'SPAN' && elt.className == 'Apple-tab-span')
{
var temp = Graph.createTabNode(4);
elt.parentNode.replaceChild(temp, elt);
elt = temp;
}
// Replaces paragraphs from macOS TextEdit
if (elt.nodeName == 'P' && elt.className == 'p1')
{
while (elt.firstChild != null)
{
elt.parentNode.insertBefore(elt.firstChild, elt);
}
if (next != null && next.nodeName == 'P' &&
next.className == 'p1')
{
elt.parentNode.insertBefore(elt.ownerDocument.
createElement('br'), elt);
}
elt.parentNode.removeChild(elt);
}
// Replaces tabs
if (!ignoreTabs && elt.innerHTML != null)
{
var tabNode = Graph.createTabNode(4);
elt.innerHTML = elt.innerHTML.replace(/\t/g,
tabNode.outerHTML);
}
}
elt = next;
}
};
/**
* Removes formatting from pasted HTML.
*/
Graph.createTabNode = function(spaces)
{
var str = '\t';
if (spaces != null)
{
str = '';
while (spaces > 0)
{
str += '\xa0';
spaces--;
}
}
// LATER: Fix normalized tab after editing plain text labels
var tabNode = document.createElement('span');
tabNode.style.whiteSpace = 'pre';
tabNode.appendChild(document.createTextNode(str));
return tabNode;
};
/**
* Sanitizes the given HTML markup, allowing target attributes and
* data: protocol links to pages and custom actions.
*/
Graph.sanitizeHtml = function(value, editing)
{
return Graph.domPurify(value, false);
};
/**
* Returns the size of the page format scaled with the page size.
*/
Graph.sanitizeLink = function(href)
{
if (href == null)
{
return null;
}
else
{
var a = document.createElement('a');
a.setAttribute('href', href);
Graph.sanitizeNode(a);
return a.getAttribute('href');
}
};
/**
* Sanitizes the given DOM node in-place.
*/
Graph.sanitizeNode = function(value)
{
return Graph.domPurify(value, true);
};
// Allows use tag in SVG with local references only
DOMPurify.addHook('afterSanitizeAttributes', function(node)
{
if (node.nodeName == 'use' && ((node.getAttribute('xlink:href') != null &&
!node.getAttribute('xlink:href').startsWith('#')) ||
(node.getAttribute('href') != null && !node.getAttribute('href').startsWith('#'))))
{
node.remove();
}
});
// Workaround for removed content with empty nodes
DOMPurify.addHook('uponSanitizeAttribute', function (node, evt)
{
if (node.nodeName == 'svg' && evt.attrName == 'content')
{
evt.forceKeepAttr = true;
}
return node;
});
/**
* Sanitizes the given value.
*/
Graph.domPurify = function(value, inPlace)
{
window.DOM_PURIFY_CONFIG.IN_PLACE = inPlace;
return DOMPurify.sanitize(value, window.DOM_PURIFY_CONFIG);
};
/**
* Updates the viewbox, width and height in the given SVG data URI
* and returns the updated data URI with all script tags and event
* handlers removed.
*/
Graph.clipSvgDataUri = function(dataUri, ignorePreserveAspect)
{
// LATER Add workaround for non-default NS declarations with empty URI not allowed in IE11
if (!mxClient.IS_IE && !mxClient.IS_IE11 && dataUri != null &&
dataUri.substring(0, 26) == 'data:image/svg+xml;base64,')
{
try
{
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.visibility = 'hidden';
// Adds the text and inserts into DOM for updating of size
var data = decodeURIComponent(escape(atob(dataUri.substring(26))));
var idx = data.indexOf('