mxgraph-map-fix
Version:
mxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.
1,748 lines (1,496 loc) • 219 kB
JavaScript
/**
* Copyright (c) 2006-2012, JGraph Ltd
*/
// Workaround for allowing target="_blank" in HTML sanitizer
// see https://code.google.com/p/google-caja/issues/detail?can=2&q=&colspec=ID%20Type%20Status%20Priority%20Owner%20Summary&groupby=&sort=&id=1296
if (typeof html4 !== 'undefined')
{
html4.ATTRIBS["a::target"] = 0;
html4.ATTRIBS["source::src"] = 0;
html4.ATTRIBS["video::src"] = 0;
// Would be nice for tooltips but probably a security risk...
//html4.ATTRIBS["video::autoplay"] = 0;
//html4.ATTRIBS["video::autobuffer"] = 0;
}
/**
* Sets global constants.
*/
// Changes default colors
mxConstants.SHADOW_OPACITY = 0.25;
mxConstants.SHADOWCOLOR = '#000000';
mxConstants.VML_SHADOWCOLOR = '#d0d0d0';
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.gridColor = '#e0e0e0';
// Alternative text for unsupported foreignObjects
mxSvgCanvas2D.prototype.foAltText = '[Not supported by viewer]';
/**
* 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)
{
mxGraph.call(this, container, model, renderHint, stylesheet);
this.themes = themes || this.defaultThemes;
this.currentEdgeStyle = mxUtils.clone(this.defaultEdgeStyle);
this.currentVertexStyle = mxUtils.clone(this.defaultVertexStyle);
// 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 state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return style['html'] == '1' || style[mxConstants.STYLE_WHITE_SPACE] == 'wrap';
};
// Implements a listener for hover and click handling on edges
if (this.edgeMode)
{
var start = {
point: null,
event: null,
state: null,
handle: null,
selected: 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');
if (!mxEvent.isControlDown(me.getEvent()) && !mxEvent.isShiftDown(me.getEvent()))
{
var state = me.getState();
if (state != null)
{
// Checks if state was removed in call to stopEditing above
if (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);
}
}
}
}
}
}
}));
var mouseDown = null;
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.isControlDown(me.getEvent()) &&
!mxEvent.isShiftDown(me.getEvent()) && !mxEvent.isAltDown(me.getEvent()))
{
var tol = this.tolerance;
if (start.point != null && start.state != null && start.event != null)
{
var state = start.state;
if (Math.abs(start.point.x - me.getGraphX()) > tol ||
Math.abs(start.point.y - me.getGraphY()) > tol)
{
// Lazy selection for edges inside groups
if (!this.isCellSelected(state.cell))
{
this.setSelectionCell(state.cell);
}
var handler = this.selectionCellsHandler.getHandler(state.cell);
if (handler != null && handler.bends != null && handler.bends.length > 0)
{
var handle = handler.getHandleForEvent(start.event);
var edgeStyle = this.view.getEdgeStyle(state);
var entity = edgeStyle == mxEdgeStyle.EntityRelation;
// 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 (!entity || handle == 0 || handle == handler.bends.length - 1 || handle == mxEvent.LABEL_HANDLE)
{
// Source or target handle or connected for direct handle access or orthogonal line
// with just two points where the central handle is moved regardless of mouse position
if (handle == mxEvent.LABEL_HANDLE || handle == 0 || state.visibleSourceState != null ||
handle == handler.bends.length - 1 || state.visibleTargetState != null)
{
if (!entity && handle != mxEvent.LABEL_HANDLE)
{
var pts = state.absolutePoints;
// Default case where handles are at corner points handles
// drag of corner as drag of existing point
if (pts != null && ((edgeStyle == null && handle == null) ||
edgeStyle == mxEdgeStyle.OrthConnector))
{
// 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
{
// 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;
}
}
handler.start(me.getGraphX(), me.getGraphX(), handle);
start.state = null;
start.event = null;
start.point = null;
start.handle = null;
start.selected = false;
me.consume();
// Removes preview rectangle in graph handler
this.graphHandler.reset();
}
}
else if (entity && (state.visibleSourceState != null || state.visibleTargetState != null))
{
// Disables moves on entity to make it consistent
this.graphHandler.reset();
me.consume();
}
}
}
}
else
{
// Updates cursor for unselected edges under the mouse
var state = me.getState();
if (state != null)
{
// Checks if state was removed in call to stopEditing above
if (this.model.isEdge(state.cell))
{
var cursor = null;
var pts = state.absolutePoints;
if (pts != null)
{
var box = new mxRectangle(me.getGraphX(), me.getGraphY());
box.grow(mxEdgeHandler.prototype.handleImage.width / 2);
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 if (state.visibleSourceState != null || state.visibleTargetState != null)
{
// Moving is not allowed for entity relation but still indicate hover state
var tmp = this.view.getEdgeStyle(state);
cursor = 'crosshair';
if (tmp != mxEdgeStyle.EntityRelation && this.isOrthogonal(state))
{
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';
}
}
}
}
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;
})
});
}
// 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
{
result = state.view.graph.sanitizeHtml(result);
}
}
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 = 0; 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 predence 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
this.graphHandler.getCells = function(initialCell)
{
var cells = mxGraphHandler.prototype.getCells.apply(this, arguments);
var newCells = [];
for (var i = 0; i < cells.length; i++)
{
var state = this.graph.view.getState(cells[i]);
var style = (state != null) ? state.style : this.graph.getCellStyle(cells[i]);
if (mxUtils.getValue(style, 'part', '0') == '1')
{
var parent = this.graph.model.getParent(cells[i]);
if (this.graph.model.isVertex(parent) && mxUtils.indexOf(cells, parent) < 0)
{
newCells.push(parent);
}
}
else
{
newCells.push(cells[i]);
}
}
return newCells;
};
// Handles parts of cells when cloning the source for new connections
this.connectionHandler.createTargetVertex = function(evt, source)
{
var state = this.graph.view.getState(source);
var style = (state != null) ? state.style : this.graph.getCellStyle(source);
if (mxUtils.getValue(style, 'part', false))
{
var parent = this.graph.model.getParent(source);
if (this.graph.model.isVertex(parent))
{
source = parent;
}
}
return mxConnectionHandler.prototype.createTargetVertex.apply(this, arguments);
};
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
var connectionHandleIsOutlineConnectEvent = this.connectionHandler.isOutlineConnectEvent;
this.connectionHandler.isOutlineConnectEvent = function(me)
{
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) || 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) ||
(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());
};
// Enables links if graph is "disabled" (ie. read-only)
var click = this.click;
this.click = function(me)
{
if (!this.isEnabled() && !me.isConsumed())
{
var cell = me.getCell();
if (cell != null)
{
var link = this.getLinkForCell(cell);
if (link != null)
{
window.open(link);
}
}
}
else
{
return click.apply(this, arguments);
}
};
// 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())
{
var link = this.getLinkForCell(cell);
if (link != null)
{
return 'pointer';
}
}
else
{
return getCursorForCell.apply(this, arguments);
}
};
// Changes rubberband selection to be recursive
this.selectRegion = function(rect, evt)
{
var cells = this.getAllCells(rect.x, rect.y, rect.width, rect.height);
this.selectCellsForEvent(cells, evt);
return cells;
};
// Recursive implementation for rubberband selection
this.getAllCells = 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) && mxUtils.getValue(state.style, 'locked', '0') != '1')
{
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);
}
this.getAllCells(x, y, width, height, cell, result);
}
}
}
}
return result;
};
// 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);
};
// Unlocks all cells
this.isCellLocked = function(cell)
{
var pState = this.view.getState(cell);
while (pState != null)
{
if (mxUtils.getValue(pState.style, 'locked', '0') == '1')
{
return true;
}
pState = this.view.getState(this.model.getParent(pState.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
this.connectionHandler.constraintHandler.isStateIgnored = function(state, source)
{
return source && state.view.graph.isCellSelected(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();
}
/**
* Adds locking
*/
var graphUpdateMouseEvent = this.updateMouseEvent;
this.updateMouseEvent = function(me)
{
me = graphUpdateMouseEvent.apply(this, arguments);
if (this.isCellLocked(me.getCell()))
{
me.state = null;
}
return me;
};
}
};
/**
* 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');
/**
* Default size for line jumps.
*/
Graph.lineJumpsEnabled = true;
/**
* Default size for line jumps.
*/
Graph.defaultJumpSize = 6;
/**
* Helper function (requires atob).
*/
Graph.createSvgImage = function(w, h, data)
{
return new mxImage('data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + w + 'px" height="' + h + 'px" ' +
'version="1.1">' + data + '</svg>'))), w, h)
};
/**
* Graph inherits from mxGraph.
*/
mxUtils.extend(Graph, mxGraph);
/**
* Allows all values in fit.
*/
Graph.prototype.minFitScale = null;
/**
* Allows all values in fit.
*/
Graph.prototype.maxFitScale = null;
/**
* Sets the policy for links. Possible values are "self" to replace any framesets,
* "blank" to load the URL in <linkTarget> and "auto" (default).
*/
Graph.prototype.linkPolicy = (urlParams['target'] == 'frame') ? 'blank' : (urlParams['target'] || 'auto');
/**
* Target for links that open in a new window. Default is _blank.
*/
Graph.prototype.linkTarget = (urlParams['target'] == 'frame') ? '_self' : '_blank';
/**
* Scrollbars are enabled on non-touch devices (not including Firefox because touch events
* cannot be detected in Firefox, see above).
*/
Graph.prototype.defaultScrollbars = !mxClient.IS_IOS;
/**
* Specifies if the page should be visible for new files. Default is true.
*/
Graph.prototype.defaultPageVisible = true;
/**
* Specifies if the app should run in chromeless mode. Default is false.
* This default is only used if the contructor argument is null.
*/
Graph.prototype.lightbox = false;
/**
*
*/
Graph.prototype.defaultGraphBackground = '#ffffff';
/**
* Specifies the size of the size for "tiles" to be used for a graph with
* scrollbars but no visible background page. A good value is large
* enough to reduce the number of repaints that is caused for auto-
* translation, which depends on this value, and small enough to give
* a small empty buffer around the graph. Default is 400x400.
*/
Graph.prototype.scrollTileSize = new mxRectangle(0, 0, 400, 400);
/**
* Overrides the background color and paints a transparent background.
*/
Graph.prototype.transparentBackground = true;
/**
* Sets the default target for all links in cells.
*/
Graph.prototype.defaultEdgeLength = 80;
/**
* Disables move of bends/segments without selecting.
*/
Graph.prototype.edgeMode = false;
/**
* Allows all values in fit.
*/
Graph.prototype.connectionArrowsEnabled = true;
/**
* Specifies the regular expression for matching placeholders.
*/
Graph.prototype.placeholderPattern = new RegExp('%(date\{.*\}|[^%^\{^\}]+)%', 'g');
/**
* Specifies the regular expression for matching placeholders.
*/
Graph.prototype.absoluteUrlPattern = new RegExp('^(?:[a-z]+:)?//', 'i');
/**
* Specifies the default name for the theme. Default is 'default'.
*/
Graph.prototype.defaultThemeName = 'default';
/**
* Specifies the default name for the theme. Default is 'default'.
*/
Graph.prototype.defaultThemes = {};
/**
* Base URL for relative links.
*/
Graph.prototype.baseUrl = ((window != window.top) ? document.referrer : document.location.toString()).split('#')[0];
/**
* Installs child layout styles.
*/
Graph.prototype.init = function(container)
{
mxGraph.prototype.init.apply(this, arguments);
// Intercepts links with no target attribute and opens in new window
this.cellRenderer.initializeLabel = function(state, shape)
{
mxCellRenderer.prototype.initializeLabel.apply(this, arguments);
// Checks tolerance for clicks on links
var tol = state.view.graph.tolerance;
var handleClick = true;
var first = null;
var down = mxUtils.bind(this, function(evt)
{
handleClick = true;
first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
});
var move = mxUtils.bind(this, function(evt)
{
handleClick = handleClick && first != null &&
Math.abs(first.x - mxEvent.getClientX(evt)) < tol &&
Math.abs(first.y - mxEvent.getClientY(evt)) < tol;
});
var up = mxUtils.bind(this, function(evt)
{
if (handleClick)
{
var elt = mxEvent.getSource(evt)
while (elt != null && elt != shape.node)
{
if (elt.nodeName.toLowerCase() == 'a')
{
state.view.graph.labelLinkClicked(state, elt, evt);
break;
}
elt = elt.parentNode;
}
}
});
mxEvent.addGestureListeners(shape.node, down, move, up);
mxEvent.addListener(shape.node, 'click', function(evt)
{
mxEvent.consume(evt);
});
};
this.initLayoutManager();
};
/**
* Adds support for page links.
*/
Graph.prototype.isPageLink = function(href)
{
return false;
};
/**
* Installs automatic layout via styles
*/
Graph.prototype.labelLinkClicked = function(state, elt, evt)
{
var href = elt.getAttribute('href');
if (href != null && !this.isPageLink(href))
{
if (!this.isEnabled())
{
var target = state.view.graph.isBlankLink(href) ?
state.view.graph.linkTarget : '_top';
href = state.view.graph.getAbsoluteUrl(href);
// Workaround for blocking in same iframe
if (target == '_self' && window != window.top)
{
window.location.href = href;
}
else
{
// Avoids page reload for anchors (workaround for IE but used everywhere)
if (href.substring(0, this.baseUrl.length) == this.baseUrl &&
href.charAt(this.baseUrl.length) == '#' &&
target == '_top' && window == window.top)
{
window.location.hash = href.split('#')[1];
}
else if ((mxEvent.isLeftMouseButton(evt) &&
!mxEvent.isPopupTrigger(evt)) ||
mxEvent.isTouchEvent(evt))
{
window.open(href, target);
}
}
}
mxEvent.consume(evt);
}
};
/**
* Returns true if the fiven href references an external protocol that
* should never open in a new window. Default returns true for mailto.
*/
Graph.prototype.isExternalProtocol = function(href)
{
return href.substring(0, 7) === 'mailto:';
};
/**
* Hook for links to open in same window. Default returns true for anchors,
* links to same domain or if target == 'self' in the config.
*/
Graph.prototype.isBlankLink = function(href)
{
return !this.isExternalProtocol(href) &&
(this.linkPolicy === 'blank' ||
(this.linkPolicy !== 'self' &&
!this.isRelativeUrl(href) &&
href.substring(0, this.domainUrl.length) !== this.domainUrl));
};
/**
*
*/
Graph.prototype.isRelativeUrl = function(url)
{
return url != null && !this.absoluteUrlPattern.test(url) &&
url.substring(0, 5) !== 'data:' &&
!this.isExternalProtocol(url);
};
/**
* Installs automatic layout via styles
*/
Graph.prototype.initLayoutManager = function()
{
this.layoutManager = new mxLayoutManager(this);
this.layoutManager.getLayout = function(cell)
{
var state = this.graph.view.getState(cell);
var style = (state != null) ? state.style : this.graph.getCellStyle(cell);
if (style['childLayout'] == 'stackLayout')
{
var stackLayout = new mxStackLayout(this.graph, true);
stackLayout.resizeParentMax = mxUtils.getValue(style, 'resizeParentMax', '1') == '1';
stackLayout.horizontal = mxUtils.getValue(style, 'horizontalStack', '1') == '1';
stackLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1';
stackLayout.resizeLast = mxUtils.getValue(style, 'resizeLast', '0') == '1';
stackLayout.spacing = style['stackSpacing'] || stackLayout.spacing;
stackLayout.border = style['stackBorder'] || stackLayout.border;
stackLayout.marginLeft = style['marginLeft'] || 0;
stackLayout.marginRight = style['marginRight'] || 0;
stackLayout.marginTop = style['marginTop'] || 0;
stackLayout.marginBottom = style['marginBottom'] || 0;
stackLayout.fill = true;
return stackLayout;
}
else if (style['childLayout'] == 'treeLayout')
{
var treeLayout = new mxCompactTreeLayout(this.graph);
treeLayout.horizontal = mxUtils.getValue(style, 'horizontalTree', '1') == '1';
treeLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1';
treeLayout.groupPadding = mxUtils.getValue(style, 'parentPadding', 20);
treeLayout.levelDistance = mxUtils.getValue(style, 'treeLevelDistance', 30);
treeLayout.maintainParentLocation = true;
treeLayout.edgeRouting = false;
treeLayout.resetEdges = false;
return treeLayout;
}
else if (style['childLayout'] == 'flowLayout')
{
var flowLayout = new mxHierarchicalLayout(this.graph, mxUtils.getValue(style,
'flowOrientation', mxConstants.DIRECTION_EAST));
flowLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1';
flowLayout.parentBorder = mxUtils.getValue(style, 'parentPadding', 20);
flowLayout.maintainParentLocation = true;
// Special undocumented styles for changing the hierarchical
flowLayout.intraCellSpacing = mxUtils.getValue(style, 'intraCellSpacing', mxHierarchicalLayout.prototype.intraCellSpacing);
flowLayout.interRankCellSpacing = mxUtils.getValue(style, 'interRankCellSpacing', mxHierarchicalLayout.prototype.interRankCellSpacing);
flowLayout.interHierarchySpacing = mxUtils.getValue(style, 'interHierarchySpacing', mxHierarchicalLayout.prototype.interHierarchySpacing);
flowLayout.parallelEdgeSpacing = mxUtils.getValue(style, 'parallelEdgeSpacing', mxHierarchicalLayout.prototype.parallelEdgeSpacing);
return flowLayout;
}
return null;
};
};
/**
* Returns the size of the page format scaled with the page size.
*/
Graph.prototype.getPageSize = function()
{
return (this.pageVisible) ? new mxRectangle(0, 0, this.pageFormat.width * this.pageScale,
this.pageFormat.height * this.pageScale) : this.scrollTileSize;
};
/**
* Returns a rectangle describing the position and count of the
* background pages, where x and y are the position of the top,
* left page and width and height are the vertical and horizontal
* page count.
*/
Graph.prototype.getPageLayout = function()
{
var size = this.getPageSize();
var bounds = this.getGraphBounds();
if (bounds.width == 0 || bounds.height == 0)
{
return new mxRectangle(0, 0, 1, 1);
}
else
{
// Computes untransformed graph bounds
var x = Math.ceil(bounds.x / this.view.scale - this.view.translate.x);
var y = Math.ceil(bounds.y / this.view.scale - this.view.translate.y);
var w = Math.floor(bounds.width / this.view.scale);
var h = Math.floor(bounds.height / this.view.scale);
var x0 = Math.floor(x / size.width);
var y0 = Math.floor(y / size.height);
var w0 = Math.ceil((x + w) / size.width) - x0;
var h0 = Math.ceil((y + h) / size.height) - y0;
return new mxRectangle(x0, y0, w0, h0);
}
};
/**
* Sanitizes the given HTML markup.
*/
Graph.prototype.sanitizeHtml = function(value, editing)
{
// Uses https://code.google.com/p/google-caja/wiki/JsHtmlSanitizer
// NOTE: Original minimized sanitizer was modified to support
// data URIs for images, mailto and special data:-links.
// LATER: Add MathML to whitelisted tags
function urlX(link)
{
if (link != null && link.toString().toLowerCase().substring(0, 11) !== 'javascript:')
{
return link;
}
return null;
};
function idX(id) { return id };
return html_sanitize(value, urlX, idX);
};
/**
* Revalidates all cells with placeholders in the current graph model.
*/
Graph.prototype.updatePlaceholders = function()
{
var model = this.model;
var validate = false;
for (var key in this.model.cells)
{
var cell = this.model.cells[key];
if (this.isReplacePlaceholders(cell))
{
this.view.invalidate(cell, false, false);
validate = true;
}
}
if (validate)
{
this.view.validate();
}
};
/**
* Adds support for placeholders in labels.
*/
Graph.prototype.isReplacePlaceholders = function(cell)
{
return cell.value != null && typeof(cell.value) == 'object' &&
cell.value.getAttribute('placeholders') == '1';
};
/**
* Adds Alt+click to select cells behind cells.
*/
Graph.prototype.isTransparentClickEvent = function(evt)
{
return mxEvent.isAltDown(evt);
};
/**
* Adds ctrl+shift+connect to disable connections.
*/
Graph.prototype.isIgnoreTerminalEvent = function(evt)
{
return mxEvent.isShiftDown(evt) && mxEvent.isControlDown(evt);
};
/**
* Adds support for placeholders in labels.
*/
Graph.prototype.isSplitTarget = function(target, cells, evt)
{
return !this.model.isEdge(cells[0]) &&
!mxEvent.isAltDown(evt) && !mxEvent.isShiftDown(evt) &&
mxGraph.prototype.isSplitTarget.apply(this, arguments);
};
/**
* Adds support for placeholders in labels.
*/
Graph.prototype.getLabel = function(cell)
{
var result = mxGraph.prototype.getLabel.apply(this, arguments);
if (result != null && this.isReplacePlaceholders(cell) && cell.getAttribute('placeholder') == null)
{
result = this.replacePlaceholders(cell, result);
}
return result;
};
/**
* Adds labelMovable style.
*/
Graph.prototype.isLabelMovable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return !this.isCellLocked(cell) &&
((this.model.isEdge(cell) && this.edgeLabelsMovable) ||
(this.model.isVertex(cell) && (this.vertexLabelsMovable ||
mxUtils.getValue(style, 'labelMovable', '0') == '1')));
};
/**
* Adds event if grid size is changed.
*/
Graph.prototype.setGridSize = function(value)
{
this.gridSize = value;
this.fireEvent(new mxEventObject('gridSizeChanged'));
};
/**
* Private helper method.
*/
Graph.prototype.getGlobalVariable = function(name)
{
var val = null;
if (name == 'date')
{
val = new Date().toLocaleDateString();
}
else if (name == 'time')
{
val = new Date().toLocaleTimeString();
}
else if (name == 'timestamp')
{
val = new Date().toLocaleString();
}
else if (name.substring(0, 5) == 'date{')
{
var fmt = name.substring(5, name.length - 1);
val = this.formatDate(new Date(), fmt);
}
return val;
};
/**
* Formats a date, see http://blog.stevenlevithan.com/archives/date-time-format
*/
Graph.prototype.formatDate = function(date, mask, utc)
{
// LATER: Cache regexs
if (this.dateFormatCache == null)
{
this.dateFormatCache = {
i18n: {
dayNames: [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
],
monthNames: [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
]
},
masks: {
"default": "ddd mmm dd yyyy HH:MM:ss",
shortDate: "m/d/yy",
mediumDate: "mmm d, yyyy",
longDate: "mmmm d, yyyy",
fullDate: "dddd, mmmm d, yyyy",
shortTime: "h:MM TT",
mediumTime: "h:MM:ss TT",
longTime: "h:MM:ss TT Z",
isoDate: "yyyy-mm-dd",
isoTime: "HH:MM:ss",
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
}
};
}
var dF = this.dateFormatCache;
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
timezoneClip = /[^-+\dA-Z]/g,
pad = function (val, len) {
val = String(val);
len = len || 2;
while (val.length < len) val = "0" + val;
return val;
};
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
mask = date;
date = undefined;
}
// Passing date through Date applies Date.parse, if necessary
date = date ? new Date(date) : new Date;
if (isNaN(date)) throw SyntaxError("invalid date");
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
// Allow setting the utc argument via the mask
if (mask.slice(0, 4) == "UTC:") {
mask = mask.slice(4);
utc = true;
}
var _ = utc ? "getUTC" : "get",
d = date[_ + "Date"](),
D = date[_ + "Day"](),
m = date[_ + "Month"](),
y = date[_ + "FullYear"](),
H = date[_ + "Hours"](),
M = date[_ + "Minutes"](),
s = date[_ + "Seconds"](),
L = date[_ + "Milliseconds"](),
o = utc ? 0 : date.getTimezoneOffset(),
flags = {
d: d,
dd: pad(d),
ddd: dF.i18n.dayNames[D],
dddd: dF.i18n.dayNames[D + 7],
m: m + 1,
mm: pad(m + 1),
mmm: dF.i18n.monthNames[m],
mmmm: dF.i18n.monthNames[m + 12],
yy: String(y).slice(2),
yyyy: y,
h: H % 12 || 12,
hh: pad(H % 12 || 12),
H: H,
HH: pad(H),
M: M,
MM: pad(M),
s: s,
ss: pad(s),
l: pad(L, 3),
L: pad(L > 99 ? Math.round(L / 10) : L),
t: H < 12 ? "a" : "p",
tt: H < 12 ? "am" : "pm",
T: H < 12 ? "A" : "P",
TT: H < 12 ? "AM" : "PM",
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
};
return mask.replace(token, function ($0)
{
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
});
};
/**
*
*/
Graph.prototype.createLayersDialog = function()
{
var div = document.createElement('div');
div.style.position = 'absolute';
var model = this.getModel();
var childCount = model.getChildCount(model.root);
for (var i = 0; i < childCount; i++)
{
(function(layer)
{
var span = document.createElement('div');
span.style.overflow = 'hidden';
span.style.textOverflow = 'ellipsis';
span.style.padding = '2px';
span.style.whiteSpace = 'nowrap';
var cb = document.createElement('input');
cb.style.display = 'inline-block';
cb.setAttribute('type', 'checkbox');
if (model.isVisible(layer))
{
cb.setAttribute('checked', 'checked');
cb.defaultChecked = true;
}
span.appendChild(cb);
var title = layer.value || (mxResources.get('background') || 'Background');
span.setAttribute('title', title);
mxUtils.write(span, title);
div.appendChild(span);
mxEvent.addListener(cb, 'click', function()
{
if (cb.getAttribute('checked') != null)
{
cb.removeAttribute('checked');
}
else
{
cb.setAttribute('checked', 'checked');
}
model.setVisible(layer, cb.checked);
});
}(model.getChildAt(model.root, i)));
}
return div;
};
/**
* Private helper method.
*/
Graph.prototype.replacePlaceholders = function(cell, str)
{
var result = [];
var last = 0;
var math = [];
while (match = this.placeholderPattern.exec(str))
{
var val = match[0];
if (val.length > 2 && val != '%label%' && val != '%tooltip%')
{
var tmp = null;
if (match.index > last && str.charAt(match.index - 1) == '%')
{
tmp = val.substring(1);
}
else
{
var name = val.substring(1, val.length - 1);
// Workaround for invalid char for getting attribute in older versions of IE
if (name.indexOf('{') < 0)
{
var current = cell;
while (tmp == null && current != null)
{
if (current.value != null && typeof(current.value) == 'object')
{
tmp = (current.hasAttribute(name)) ? ((current.getAttribute(name) != null) ?
current.getAttribute(name) : '') : null;
}
current = this.model.getParent(current);
}
}
if (tmp == null)
{
tmp = this.getGlobalVariable(name);
}
}
result.push(str.substring(last, match.index) + ((tmp != null) ? tmp : val));
last = match.index + val.length;
}
}
result.push(str.substring(last));
return result.join('');
};
/**
* Selects cells for connect vertex return value.
*/
Graph.prototype.selectCellsForConnectVertex = function(cells, evt, hoverIcons)
{
// Selects only target vertex if one exists
if (cells.length == 2 && this.model.isVertex(cells[1]))
{
this.setSelectionCell(cells[1]);
if (hoverIcons != null)
{
// Adds hover icons to new target vertex for touch devices
if (mxEvent.isTouchEvent(evt))
{
hoverIcons.update(hoverIcons.getState(this.view.getState(cells[1])));
}
else
{
// Hides hover icons after click with mouse
hoverIcons.reset();
}
}
this.scrollCellToVisible(cells[1]);
}
else
{
this.setSelectionCells(cells);
}
};
/**
* Adds a connection to the given vertex.
*/
Graph.prototype.connectVertex = function(source, direction, length, evt, forceClone, ignoreCellAt)
{
ignoreCellAt = (ignoreCellAt) ? ignoreCellAt : false;
var pt = (source.geometry.relative && source.parent.geometry != null) ?
new mxPoint(source.parent.geometry.width * source.geometry.x, source.parent.geometry.height * source.geometry.y) :
new mxPoint(source.geometry.x, source.geometry.y);
if (direction == mxConstants.DIRECTION_NORTH)
{
pt.x += source.geometry.width / 2;
pt.y -= length ;
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
pt.x += source.geometry.width / 2;
pt.y += source.geometry.height + length;
}
else if (direction == mxConstants.DIRECTION_WEST)
{
pt.x -= length;
pt.y += source.geometry.height / 2;
}
else
{
pt.x += source.geometry.width + length;
pt.y += source.geometry.height / 2;
}
var parentState = this.view.getState(this.model.getParent(source));
var s = this.view.scale;
var t = this.view.translate;
var dx = t.x * s;
var dy = t.y * s;
if (this.model.isVertex(parentState.cell))
{
dx = parentState.x;
dy = parentState.y;
}
// Workaround for relative child cells
if (this.model.isVertex(source.parent) && source.geometry.relative)
{
pt.x += source.parent.geometry.x;
pt.y += source.parent.geometry.y;
}
// Checks actual end point of edge for target cell
var target = (ignoreCellAt || (mxEvent.isControlDown(evt) && !forceClone)) ?
null : this.getCellAt(dx + pt.x * s, dy + pt.y * s);
if (this.model.isAncestor(target, source))
{
target = null;
}
// Checks if target or ancestor is locked
var temp = target;
while (temp != null)
{
if (this.isCellLocked(temp))
{
target = null;
break;
}
temp = this.model.getParent(temp);
}
// Checks if source and target intersect
if (target != null)
{
var sourceState = this.view.getState(source);
var targetState = this.view.getState(target);
if (sourceState != null && targetState != null && mxUtils.intersects(sourceState, targetState))
{
target = null;
}
}
var duplicate = !mxEvent.isShiftDown(evt) || forceClone;
if (duplicate)
{
if (direction == mxConstants.DIRECTION_NORTH)
{
pt.y -= source.geometry.height / 2;
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
pt.y += source.geometry.height / 2;