/** * 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; } // 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(); }; } // Changes default colors /** * Measurements Units */ mxConstants.POINTS = 1; mxConstants.MILLIMETERS = 2; mxConstants.INCHES = 3; /** * 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'; 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.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; }; /** * 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 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 = 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 newCells = []; for (var i = 0; i < cells.length; i++) { var cell = this.graph.getCompositeParent(cells[i]); if (cell == cells[i]) { newCells.push(cells[i]); } else if (cell != null && mxUtils.indexOf(cells, cell) < 0) { newCells.push(cell); } } return newCells; }; // Handles parts of cells for drag and drop var graphHandlerStart = this.graphHandler.start; this.graphHandler.start = function(cell, x, y, cells) { 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); }; 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) { 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) && !mxEvent.isShiftDown(me.getEvent()) && !mxEvent.isControlDown(me.getEvent())) || (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 if graph is read-only or cell is locked var click = this.click; this.click = function(me) { var locked = me.state == null && me.sourceState != null && this.isCellLocked(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); } else { this.openLink(link); } } } if (this.isEnabled() && locked) { this.clearSelection(); } } else { return click.apply(this, arguments); } }; // 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 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 (me.state != null && this.isCellLocked(me.getCell())) { me.state = null; } return me; }; } //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'); /** * Default size for line jumps. */ Graph.lineJumpsEnabled = true; /** * Default size for line jumps. */ Graph.defaultJumpSize = 6; /** * Minimum width for table columns. */ Graph.minTableColumnWidth = 20; /** * Minimum height for table rows. */ Graph.minTableRowHeight = 20; /** * Text for foreign object warning. */ Graph.foreignObjectWarningText = 'Viewer does not support full SVG 1.1'; /** * Link for foreign object warning. */ Graph.foreignObjectWarningLink = 'https://desk.draw.io/support/solutions/articles/16000042487'; /** * Helper function for creating SVG data URI. */ Graph.createSvgImage = function(w, h, data, coordWidth, coordHeight) { var tmp = unescape(encodeURIComponent( '' + '' + data + '')); return new mxImage('data:image/svg+xml;base64,' + ((window.btoa) ? btoa(tmp) : Base64.encode(tmp, true)), w, h) }; /** * Removes all illegal control characters with ASCII code <32 except TAB, LF * and CR. */ Graph.zapGremlins = function(text) { 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.charAt(i)); } } return 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(''); }; /** * 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 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), {to: 'string'}) : pako.deflateRaw(encodeURIComponent(data), {to: 'string'}); return (window.btoa) ? btoa(tmp) : Base64.encode(tmp, true); } }; /** * 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 = (window.atob) ? atob(data) : Base64.decode(data, true); var inflated = decodeURIComponent((inflate) ? pako.inflate(tmp, {to: 'string'}) : pako.inflateRaw(tmp, {to: 'string'})); return (checked) ? inflated : Graph.zapGremlins(inflated); } }; /** * 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 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'; /** * Value to the rel attribute of links. Default is 'nofollow noopener noreferrer'. * NOTE: There are security implications when this is changed and if noopener is removed, * then must be overridden to allow for the opener to be set by default. */ Graph.prototype.linkRelation = 'nofollow noopener noreferrer'; /** * 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.defaultPageBackgroundColor = '#ffffff'; /** * */ Graph.prototype.defaultPageBorderColor = '#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 global constants. */ Graph.prototype.selectParentAfterDelete = false; /** * 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 = (urlParams['base'] != null) ? decodeURIComponent(urlParams['base']) : (((window != window.top) ? document.referrer : document.location.toString()).split('#')[0]); /** * Specifies if the label should be edited after an insert. */ Graph.prototype.editAfterInsert = false; /** * Defines the built-in properties to be ignored in tooltips. */ Graph.prototype.builtInProperties = ['label', 'tooltip', 'placeholders', 'placeholder']; /** * Defines if the graph is part of an EditorUi. If this is false the graph can * be used in an EditorUi instance but will not have a UI added, functions * overridden or event handlers added. */ Graph.prototype.standalone = false; /** * 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(); }; /** * Implements zoom and offset via CSS transforms. This is currently only used * in read-only as there are fewer issues with the mxCellState not being scaled * and translated. * * KNOWN ISSUES TO FIX: * - Apply CSS transforms to HTML labels in IE11 */ (function() { /** * Uses CSS transforms for scale and translate. */ Graph.prototype.useCssTransforms = false; /** * Contains the scale. */ Graph.prototype.currentScale = 1; /** * Contains the offset. */ Graph.prototype.currentTranslate = new mxPoint(0, 0); /** * Returns true if fast zoom preview should be used. */ Graph.prototype.isFastZoomEnabled = function() { return urlParams['zoom'] != 'nocss' && !mxClient.NO_FO && !mxClient.IS_EDGE && !this.useCssTransforms && this.isCssTransformsSupported(); }; /** * Only foreignObject supported for now (no IE11). Safari disabled as it ignores * overflow visible on foreignObject in negative space (lightbox and viewer). */ Graph.prototype.isCssTransformsSupported = function() { return this.dialect == mxConstants.DIALECT_SVG && !mxClient.NO_FO && (!this.lightbox || !mxClient.IS_SF); }; /** * Function: getCellAt * * Needs to modify original method for recursive call. */ Graph.prototype.getCellAt = function(x, y, parent, vertices, edges, ignoreFn) { if (this.useCssTransforms) { x = x / this.currentScale - this.currentTranslate.x; y = y / this.currentScale - this.currentTranslate.y; } return this.getScaledCellAt.apply(this, arguments); }; /** * Function: getScaledCellAt * * Overridden for recursion. */ Graph.prototype.getScaledCellAt = function(x, y, parent, vertices, edges, ignoreFn) { vertices = (vertices != null) ? vertices : true; edges = (edges != null) ? edges : true; if (parent == null) { parent = this.getCurrentRoot(); if (parent == null) { parent = this.getModel().getRoot(); } } if (parent != null) { var childCount = this.model.getChildCount(parent); for (var i = childCount - 1; i >= 0; i--) { var cell = this.model.getChildAt(parent, i); var result = this.getScaledCellAt(x, y, cell, vertices, edges, ignoreFn); if (result != null) { return result; } else if (this.isCellVisible(cell) && (edges && this.model.isEdge(cell) || vertices && this.model.isVertex(cell))) { var state = this.view.getState(cell); if (state != null && (ignoreFn == null || !ignoreFn(state, x, y)) && this.intersects(state, x, y)) { return cell; } } } } return null; }; /** * Returns if the child cells of the given vertex cell state should be resized. */ Graph.prototype.isRecursiveVertexResize = function(state) { return !this.isSwimlane(state.cell) && this.model.getChildCount(state.cell) > 0 && !this.isCellCollapsed(state.cell) && mxUtils.getValue(state.style, 'recursiveResize', '1') == '1' && mxUtils.getValue(state.style, 'childLayout', null) == null; } /** * Returns the first parent that is not a part. */ Graph.prototype.isPart = function(cell) { return (!this.model.isVertex(cell)) ? false : mxUtils.getValue(this.getCurrentCellStyle(cell), 'part', '0') == '1'; }; /** * Returns the first parent that is not a part. */ Graph.prototype.getCompositeParent = function(cell) { while (this.isPart(cell)) { cell = this.model.getParent(cell); } return cell; }; /** * Function: repaint * * Updates the highlight after a change of the model or view. */ mxCellHighlight.prototype.getStrokeWidth = function(state) { var s = this.strokeWidth; if (this.graph.useCssTransforms) { s /= this.graph.currentScale; } return s; }; /** * Function: getGraphBounds * * Overrides getGraphBounds to use bounding box from SVG. */ mxGraphView.prototype.getGraphBounds = function() { var b = this.graphBounds; if (this.graph.useCssTransforms) { var t = this.graph.currentTranslate; var s = this.graph.currentScale; b = new mxRectangle( (b.x + t.x) * s, (b.y + t.y) * s, b.width * s, b.height * s); } return b; }; /** * Function: viewStateChanged * * Overrides to bypass full cell tree validation. * TODO: Check if this improves performance */ mxGraphView.prototype.viewStateChanged = function() { if (this.graph.useCssTransforms) { this.validate(); this.graph.sizeDidChange(); } else { this.revalidate(); this.graph.sizeDidChange(); } }; /** * Function: validate * * Overrides validate to normalize validation view state and pass * current state to CSS transform. */ var graphViewValidate = mxGraphView.prototype.validate; mxGraphView.prototype.validate = function(cell) { if (this.graph.useCssTransforms) { this.graph.currentScale = this.scale; this.graph.currentTranslate.x = this.translate.x; this.graph.currentTranslate.y = this.translate.y; this.scale = 1; this.translate.x = 0; this.translate.y = 0; } graphViewValidate.apply(this, arguments); if (this.graph.useCssTransforms) { this.graph.updateCssTransform(); this.scale = this.graph.currentScale; this.translate.x = this.graph.currentTranslate.x; this.translate.y = this.graph.currentTranslate.y; } }; /** * Function: updateCssTransform * * Zooms out of the graph by . */ Graph.prototype.updateCssTransform = function() { var temp = this.view.getDrawPane(); if (temp != null) { var g = temp.parentNode; if (!this.useCssTransforms) { g.removeAttribute('transformOrigin'); g.removeAttribute('transform'); } else { var prev = g.getAttribute('transform'); g.setAttribute('transformOrigin', '0 0'); var s = Math.round(this.currentScale * 100) / 100; var dx = Math.round(this.currentTranslate.x * 100) / 100; var dy = Math.round(this.currentTranslate.y * 100) / 100; g.setAttribute('transform', 'scale(' + s + ',' + s + ')' + 'translate(' + dx + ',' + dy + ')'); // Applies workarounds only if translate has changed if (prev != g.getAttribute('transform')) { try { // Applies transform to labels outside of the SVG DOM // Excluded via isCssTransformsSupported // if (mxClient.NO_FO) // { // var transform = 'scale(' + this.currentScale + ')' + 'translate(' + // this.currentTranslate.x + 'px,' + this.currentTranslate.y + 'px)'; // // this.view.states.visit(mxUtils.bind(this, function(cell, state) // { // if (state.text != null && state.text.node != null) // { // // Stores initial CSS transform that is used for the label alignment // if (state.text.originalTransform == null) // { // state.text.originalTransform = state.text.node.style.transform; // } // // state.text.node.style.transform = transform + state.text.originalTransform; // } // })); // } // Workaround for https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4320441/ if (mxClient.IS_EDGE) { // Recommended workaround is to do this on all // foreignObjects, but this seems to be faster var val = g.style.display; g.style.display = 'none'; g.getBBox(); g.style.display = val; } } catch (e) { // ignore } } } } }; var graphViewValidateBackgroundPage = mxGraphView.prototype.validateBackgroundPage; mxGraphView.prototype.validateBackgroundPage = function() { var useCssTranforms = this.graph.useCssTransforms, scale = this.scale, translate = this.translate; if (useCssTranforms) { this.scale = this.graph.currentScale; this.translate = this.graph.currentTranslate; } graphViewValidateBackgroundPage.apply(this, arguments); if (useCssTranforms) { this.scale = scale; this.translate = translate; } }; var graphUpdatePageBreaks = mxGraph.prototype.updatePageBreaks; mxGraph.prototype.updatePageBreaks = function(visible, width, height) { var useCssTranforms = this.useCssTransforms, scale = this.view.scale, translate = this.view.translate; if (useCssTranforms) { this.view.scale = 1; this.view.translate = new mxPoint(0, 0); this.useCssTransforms = false; } graphUpdatePageBreaks.apply(this, arguments); if (useCssTranforms) { this.view.scale = scale; this.view.translate = translate; this.useCssTransforms = true; } }; })(); /** * Sets the XML node for the current diagram. */ Graph.prototype.isLightboxView = function() { return this.lightbox; }; /** * Sets the XML node for the current diagram. */ Graph.prototype.isViewer = function() { return false; }; /** * Installs automatic layout via styles */ Graph.prototype.labelLinkClicked = function(state, elt, evt) { var href = elt.getAttribute('href'); if (href != null && !this.isCustomLink(href) && (mxEvent.isLeftMouseButton(evt) && !mxEvent.isPopupTrigger(evt)) || mxEvent.isTouchEvent(evt)) { if (!this.isEnabled() || this.isCellLocked(state.cell)) { var target = this.isBlankLink(href) ? this.linkTarget : '_top'; this.openLink(this.getAbsoluteUrl(href), target); } mxEvent.consume(evt); } }; /** * Returns the size of the page format scaled with the page size. */ Graph.prototype.openLink = function(href, target, allowOpener) { var result = window; try { // 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) { var hash = href.split('#')[1]; // Forces navigation if on same hash if (window.location.hash == '#' + hash) { window.location.hash = ''; } window.location.hash = hash; } else { result = window.open(href, (target != null) ? target : '_blank'); if (result != null && !allowOpener) { result.opener = null; } } } } catch (e) { // ignores permission denied } return result; }; /** * Adds support for page links. */ Graph.prototype.getLinkTitle = function(href) { return href.substring(href.lastIndexOf('/') + 1); }; /** * Adds support for page links. */ Graph.prototype.isCustomLink = function(href) { return href.substring(0, 5) == 'data:'; }; /** * Adds support for page links. */ Graph.prototype.customLinkClicked = function(link) { return false; }; /** * Returns true if the given 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); }; /** * */ Graph.prototype.getAbsoluteUrl = function(url) { if (url != null && this.isRelativeUrl(url)) { if (url.charAt(0) == '#') { url = this.baseUrl + url; } else if (url.charAt(0) == '/') { url = this.domainUrl + url; } else { url = this.domainPathUrl + url; } } return url; }; /** * Installs automatic layout via styles */ Graph.prototype.initLayoutManager = function() { this.layoutManager = new mxLayoutManager(this); // Using shared instances for table layouts var rowLayout = new TableRowLayout(this); var tableLayout = new TableLayout(this); this.layoutManager.getLayout = function(cell, eventName) { // Workaround for possible invalid style after change and before view validation if (eventName != mxEvent.BEGIN_UPDATE) { var style = this.graph.getCellStyle(cell); if (style != null) { 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; } else if (style['childLayout'] == 'circleLayout') { return new mxCircleLayout(this.graph); } else if (style['childLayout'] == 'organicLayout') { return new mxFastOrganicLayout(this.graph); } else if (this.graph.isTableRow(cell) || this.graph.isTableCell(cell)) { return rowLayout; } else if (this.graph.isTable(cell)) { return tableLayout; } } } 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 { var x0 = Math.floor(Math.ceil(bounds.x / this.view.scale - this.view.translate.x) / size.width); var y0 = Math.floor(Math.ceil(bounds.y / this.view.scale - this.view.translate.y) / size.height); var w0 = Math.ceil((Math.floor((bounds.x + bounds.width) / this.view.scale) - this.view.translate.x) / size.width) - x0; var h0 = Math.ceil((Math.floor((bounds.y + bounds.height) / this.view.scale) - this.view.translate.y) / 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'; }; /** * Returns true if the given mouse wheel event should be used for zooming. This * is invoked if no dialogs are showing and returns true with Alt or Control * (or cmd in macOS only) is pressed. */ Graph.prototype.isZoomWheelEvent = function(evt) { return mxEvent.isAltDown(evt) || (mxEvent.isMetaDown(evt) && mxClient.IS_MAC) || mxEvent.isControlDown(evt); }; /** * Returns true if the given scroll wheel event should be used for scrolling. */ Graph.prototype.isScrollWheelEvent = function(evt) { return !this.isZoomWheelEvent(evt); }; /** * Adds Alt+click to select cells behind cells (Shift+Click on Chrome OS). */ Graph.prototype.isTransparentClickEvent = function(evt) { return mxEvent.isAltDown(evt) || (mxClient.IS_CHROMEOS && mxEvent.isShiftDown(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 style = this.getCurrentCellStyle(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')); }; /** * Function: getClickableLinkForCell * * Returns the first non-null link for the cell or its ancestors. * * Parameters: * * cell - whose link should be returned. */ Graph.prototype.getClickableLinkForCell = function(cell) { do { var link = this.getLinkForCell(cell); if (link != null) { return link; } cell = this.model.getParent(cell); } while (cell != null); return null; }; /** * 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++) { (mxUtils.bind(this, 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 = this.convertValueToString(layer) || (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 = []; if (str != null) { var last = 0; 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(''); }; /** * Resolves the given cells in the model and selects them. */ Graph.prototype.restoreSelection = function(cells) { if (cells != null && cells.length > 0) { var temp = []; for (var i = 0; i < cells.length; i++) { var newCell = this.model.getCell(cells[i].id); if (newCell != null) { temp.push(newCell); } } this.setSelectionCells(temp); } else { this.clearSelection(); } }; /** * 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]); this.scrollCellToVisible(cells[1]); if (hoverIcons != null) { // Adds hover icons for cloned vertex or hides icons if (mxEvent.isTouchEvent(evt)) { hoverIcons.update(hoverIcons.getState(this.view.getState(cells[1]))); } else { hoverIcons.reset(); } } } else { this.setSelectionCells(cells); } }; /** * Adds a connection to the given vertex. */ Graph.prototype.connectVertex = function(source, direction, length, evt, forceClone, ignoreCellAt) { // Ignores relative edge labels if (source.geometry.relative && this.model.isEdge(source.parent)) { return []; } 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 (parentState != null && 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; } else if (direction == mxConstants.DIRECTION_WEST) { pt.x -= source.geometry.width / 2; } else { pt.x += source.geometry.width / 2; } } // Uses connectable parent vertex if one exists if (target != null && !this.isCellConnectable(target)) { var parent = this.getModel().getParent(target); if (this.getModel().isVertex(parent) && this.isCellConnectable(parent)) { target = parent; } } if (target == source || this.model.isEdge(target) || !this.isCellConnectable(target)) { target = null; } var result = []; this.model.beginUpdate(); try { var swimlane = target != null && this.isSwimlane(target); var realTarget = (!swimlane) ? target : null; if (realTarget == null && duplicate) { // Handles relative children var cellToClone = source; var geo = this.getCellGeometry(source); while (geo != null && geo.relative) { cellToClone = this.getModel().getParent(cellToClone); geo = this.getCellGeometry(cellToClone); } // Handle consistuents for cloning cellToClone = this.getCompositeParent(cellToClone); realTarget = this.duplicateCells([cellToClone], false)[0]; var geo = this.getCellGeometry(realTarget); if (geo != null) { geo.x = pt.x - geo.width / 2; geo.y = pt.y - geo.height / 2; } if (swimlane) { this.addCells([realTarget], target, null, null, null, true); target = null; } } // Never connects children in stack layouts var layout = null; if (this.layoutManager != null) { layout = this.layoutManager.getLayout(this.model.getParent(source)); } var edge = ((mxEvent.isControlDown(evt) && duplicate) || (target == null && layout != null && layout.constructor == mxStackLayout)) ? null : this.insertEdge(this.model.getParent(source), null, '', source, realTarget, this.createCurrentEdgeStyle()); // Inserts edge before source if (edge != null && this.connectionHandler.insertBeforeSource) { var index = null; var tmp = source; while (tmp.parent != null && tmp.geometry != null && tmp.geometry.relative && tmp.parent != edge.parent) { tmp = this.model.getParent(tmp); } if (tmp != null && tmp.parent != null && tmp.parent == edge.parent) { var index = tmp.parent.getIndex(tmp); this.model.add(tmp.parent, edge, index); } } // Special case: Click on west icon puts clone before cell if (target == null && realTarget != null && layout != null && source.parent != null && layout.constructor == mxStackLayout && direction == mxConstants.DIRECTION_WEST) { var index = source.parent.getIndex(source); this.model.add(source.parent, realTarget, index); } if (edge != null) { result.push(edge); } if (target == null && realTarget != null) { result.push(realTarget); } if (realTarget == null && edge != null) { edge.geometry.setTerminalPoint(pt, false); } if (edge != null) { this.fireEvent(new mxEventObject('cellsInserted', 'cells', [edge])); } } finally { this.model.endUpdate(); } return result; }; /** * Returns all labels in the diagram as a string. */ Graph.prototype.getIndexableText = function() { var tmp = document.createElement('div'); var labels = []; var label = ''; for (var key in this.model.cells) { var cell = this.model.cells[key]; if (this.model.isVertex(cell) || this.model.isEdge(cell)) { if (this.isHtmlLabel(cell)) { tmp.innerHTML = this.getLabel(cell); label = mxUtils.extractTextWithWhitespace([tmp]); } else { label = this.getLabel(cell); } label = mxUtils.trim(label.replace(/[\x00-\x1F\x7F-\x9F]|\s+/g, ' ')); if (label.length > 0) { labels.push(label); } } } return labels.join(' '); }; /** * Returns the label for the given cell. */ Graph.prototype.convertValueToString = function(cell) { var value = this.model.getValue(cell); if (value != null && typeof(value) == 'object') { if (this.isReplacePlaceholders(cell) && cell.getAttribute('placeholder') != null) { var name = cell.getAttribute('placeholder'); var current = cell; var result = null; while (result == null && current != null) { if (current.value != null && typeof(current.value) == 'object') { result = (current.hasAttribute(name)) ? ((current.getAttribute(name) != null) ? current.getAttribute(name) : '') : null; } current = this.model.getParent(current); } return result || ''; } else { return value.getAttribute('label') || ''; } } return mxGraph.prototype.convertValueToString.apply(this, arguments); }; /** * Returns the link for the given cell. */ Graph.prototype.getLinksForState = function(state) { if (state != null && state.text != null && state.text.node != null) { return state.text.node.getElementsByTagName('a'); } return null; }; /** * Returns the link for the given cell. */ Graph.prototype.getLinkForCell = function(cell) { if (cell.value != null && typeof(cell.value) == 'object') { var link = cell.value.getAttribute('link'); // Removes links with leading javascript: protocol // TODO: Check more possible attack vectors if (link != null && link.toLowerCase().substring(0, 11) === 'javascript:') { link = link.substring(11); } return link; } return null; }; /** * Overrides label orientation for collapsed swimlanes inside stack. */ Graph.prototype.getCellStyle = function(cell) { var style = mxGraph.prototype.getCellStyle.apply(this, arguments); if (cell != null && this.layoutManager != null) { var parent = this.model.getParent(cell); if (this.model.isVertex(parent) && this.isCellCollapsed(cell)) { var layout = this.layoutManager.getLayout(parent); if (layout != null && layout.constructor == mxStackLayout) { style[mxConstants.STYLE_HORIZONTAL] = !layout.horizontal; } } } return style; }; /** * Disables alternate width persistence for stack layout parents */ Graph.prototype.updateAlternateBounds = function(cell, geo, willCollapse) { if (cell != null && geo != null && this.layoutManager != null && geo.alternateBounds != null) { var layout = this.layoutManager.getLayout(this.model.getParent(cell)); if (layout != null && layout.constructor == mxStackLayout) { if (layout.horizontal) { geo.alternateBounds.height = 0; } else { geo.alternateBounds.width = 0; } } } mxGraph.prototype.updateAlternateBounds.apply(this, arguments); }; /** * Adds Shift+collapse/expand and size management for folding inside stack */ Graph.prototype.isMoveCellsEvent = function(evt, state) { return mxEvent.isShiftDown(evt) || mxUtils.getValue(state.style, 'moveCells', '0') == '1'; }; /** * Adds Shift+collapse/expand and size management for folding inside stack */ Graph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable, evt) { recurse = (recurse != null) ? recurse : false; if (cells == null) { cells = this.getFoldableCells(this.getSelectionCells(), collapse); } if (cells != null) { this.model.beginUpdate(); try { mxGraph.prototype.foldCells.apply(this, arguments); // Resizes all parent stacks if alt is not pressed if (this.layoutManager != null) { for (var i = 0; i < cells.length; i++) { var state = this.view.getState(cells[i]); var geo = this.getCellGeometry(cells[i]); if (state != null && geo != null) { var dx = Math.round(geo.width - state.width / this.view.scale); var dy = Math.round(geo.height - state.height / this.view.scale); if (dy != 0 || dx != 0) { var parent = this.model.getParent(cells[i]); var layout = this.layoutManager.getLayout(parent); if (layout == null) { // Moves cells to the right and down after collapse/expand if (evt != null && this.isMoveCellsEvent(evt, state)) { this.moveSiblings(state, parent, dx, dy); } } else if ((evt == null || !mxEvent.isAltDown(evt)) && layout.constructor == mxStackLayout && !layout.resizeLast) { this.resizeParentStacks(parent, layout, dx, dy); } } } } } } finally { this.model.endUpdate(); } // Selects cells after folding if (this.isEnabled()) { this.setSelectionCells(cells); } } }; /** * Overrides label orientation for collapsed swimlanes inside stack. */ Graph.prototype.moveSiblings = function(state, parent, dx, dy) { this.model.beginUpdate(); try { var cells = this.getCellsBeyond(state.x, state.y, parent, true, true); for (var i = 0; i < cells.length; i++) { if (cells[i] != state.cell) { var tmp = this.view.getState(cells[i]); var geo = this.getCellGeometry(cells[i]); if (tmp != null && geo != null) { geo = geo.clone(); geo.translate(Math.round(dx * Math.max(0, Math.min(1, (tmp.x - state.x) / state.width))), Math.round(dy * Math.max(0, Math.min(1, (tmp.y - state.y) / state.height)))); this.model.setGeometry(cells[i], geo); } } } } finally { this.model.endUpdate(); } }; /** * Overrides label orientation for collapsed swimlanes inside stack. */ Graph.prototype.resizeParentStacks = function(parent, layout, dx, dy) { if (this.layoutManager != null && layout != null && layout.constructor == mxStackLayout && !layout.resizeLast) { this.model.beginUpdate(); try { var dir = layout.horizontal; // Bubble resize up for all parent stack layouts with same orientation while (parent != null && layout != null && layout.constructor == mxStackLayout && layout.horizontal == dir && !layout.resizeLast) { var pgeo = this.getCellGeometry(parent); var pstate = this.view.getState(parent); if (pstate != null && pgeo != null) { pgeo = pgeo.clone(); if (layout.horizontal) { pgeo.width += dx + Math.min(0, pstate.width / this.view.scale - pgeo.width); } else { pgeo.height += dy + Math.min(0, pstate.height / this.view.scale - pgeo.height); } this.model.setGeometry(parent, pgeo); } parent = this.model.getParent(parent); layout = this.layoutManager.getLayout(parent); } } finally { this.model.endUpdate(); } } }; /** * Disables drill-down for non-swimlanes. */ Graph.prototype.isContainer = function(cell) { var style = this.getCurrentCellStyle(cell); if (this.isSwimlane(cell)) { return style['container'] != '0'; } else { return style['container'] == '1'; } }; /** * Adds a expand style. */ Graph.prototype.isExtendParent = function(cell) { var parent = this.model.getParent(cell); if (parent != null) { var style = this.getCurrentCellStyle(parent); if (style['expand'] != null) { return style['expand'] != '0'; } } return mxGraph.prototype.isExtendParent.apply(this, arguments); }; /** * Adds a connectable style. */ Graph.prototype.isCellConnectable = function(cell) { var style = this.getCurrentCellStyle(cell); return (style['connectable'] != null) ? style['connectable'] != '0' : mxGraph.prototype.isCellConnectable.apply(this, arguments); }; /** * Adds labelMovable style. */ Graph.prototype.isLabelMovable = function(cell) { var style = this.getCurrentCellStyle(cell); return (style['movableLabel'] != null) ? style['movableLabel'] != '0' : mxGraph.prototype.isLabelMovable.apply(this, arguments); }; /** * Function: selectAll * * Selects all children of the given parent cell or the children of the * default parent if no parent is specified. To select leaf vertices and/or * edges use . * * Parameters: * * parent - Optional whose children should be selected. * Default is . */ Graph.prototype.selectAll = function(parent) { parent = parent || this.getDefaultParent(); if (!this.isCellLocked(parent)) { mxGraph.prototype.selectAll.apply(this, arguments); } }; /** * Function: selectCells * * Selects all vertices and/or edges depending on the given boolean * arguments recursively, starting at the given parent or the default * parent if no parent is specified. Use to select all cells. * For vertices, only cells with no children are selected. * * Parameters: * * vertices - Boolean indicating if vertices should be selected. * edges - Boolean indicating if edges should be selected. * parent - Optional that acts as the root of the recursion. * Default is . */ Graph.prototype.selectCells = function(vertices, edges, parent) { parent = parent || this.getDefaultParent(); if (!this.isCellLocked(parent)) { mxGraph.prototype.selectCells.apply(this, arguments); } }; /** * Function: getSwimlaneAt * * Returns the bottom-most swimlane that intersects the given point (x, y) * in the cell hierarchy that starts at the given parent. * * Parameters: * * x - X-coordinate of the location to be checked. * y - Y-coordinate of the location to be checked. * parent - that should be used as the root of the recursion. * Default is . */ Graph.prototype.getSwimlaneAt = function (x, y, parent) { var result = mxGraph.prototype.getSwimlaneAt.apply(this, arguments); if (this.isCellLocked(result)) { result = null; } return result; }; /** * Disables folding for non-swimlanes. */ Graph.prototype.isCellFoldable = function(cell) { var style = this.getCurrentCellStyle(cell); return this.foldingEnabled && (style['treeFolding'] == '1' || (!this.isCellLocked(cell) && ((this.isContainer(cell) && style['collapsible'] != '0') || (!this.isContainer(cell) && style['collapsible'] == '1')))); }; /** * Stops all interactions and clears the selection. */ Graph.prototype.reset = function() { if (this.isEditing()) { this.stopEditing(true); } this.escape(); if (!this.isSelectionEmpty()) { this.clearSelection(); } }; /** * Overridden to limit zoom to 1% - 16.000%. */ Graph.prototype.zoom = function(factor, center) { factor = Math.max(0.01, Math.min(this.view.scale * factor, 160)) / this.view.scale; mxGraph.prototype.zoom.apply(this, arguments); }; /** * Function: zoomIn * * Zooms into the graph by . */ Graph.prototype.zoomIn = function() { // Switches to 1% zoom steps below 15% if (this.view.scale < 0.15) { this.zoom((this.view.scale + 0.01) / this.view.scale); } else { // Uses to 5% zoom steps for better grid rendering in webkit // and to avoid rounding errors for zoom steps this.zoom((Math.round(this.view.scale * this.zoomFactor * 20) / 20) / this.view.scale); } }; /** * Function: zoomOut * * Zooms out of the graph by . */ Graph.prototype.zoomOut = function() { // Switches to 1% zoom steps below 15% if (this.view.scale <= 0.15) { this.zoom((this.view.scale - 0.01) / this.view.scale); } else { // Uses to 5% zoom steps for better grid rendering in webkit // and to avoid rounding errors for zoom steps this.zoom((Math.round(this.view.scale * (1 / this.zoomFactor) * 20) / 20) / this.view.scale); } }; /** * Overrides tooltips to show custom tooltip or metadata. */ Graph.prototype.getTooltipForCell = function(cell) { var tip = ''; if (mxUtils.isNode(cell.value)) { var tmp = cell.value.getAttribute('tooltip'); if (tmp != null) { if (tmp != null && this.isReplacePlaceholders(cell)) { tmp = this.replacePlaceholders(cell, tmp); } tip = this.sanitizeHtml(tmp); } else { var ignored = this.builtInProperties; var attrs = cell.value.attributes; var temp = []; // Hides links in edit mode if (this.isEnabled()) { ignored.push('link'); } for (var i = 0; i < attrs.length; i++) { if (mxUtils.indexOf(ignored, attrs[i].nodeName) < 0 && attrs[i].nodeValue.length > 0) { temp.push({name: attrs[i].nodeName, value: attrs[i].nodeValue}); } } // Sorts by name temp.sort(function(a, b) { if (a.name < b.name) { return -1; } else if (a.name > b.name) { return 1; } else { return 0; } }); for (var i = 0; i < temp.length; i++) { if (temp[i].name != 'link' || !this.isCustomLink(temp[i].value)) { tip += ((temp[i].name != 'link') ? '' + temp[i].name + ': ' : '') + mxUtils.htmlEntities(temp[i].value) + '\n'; } } if (tip.length > 0) { tip = tip.substring(0, tip.length - 1); if (mxClient.IS_SVG) { tip = '
' + tip + '
'; } } } } return tip; }; /** * Turns the given string into an array. */ Graph.prototype.stringToBytes = function(str) { return Graph.stringToBytes(str); }; /** * Turns the given array into a string. */ Graph.prototype.bytesToString = function(arr) { return Graph.bytesToString(arr); }; /** * Returns a base64 encoded version of the compressed outer XML of the given node. */ Graph.prototype.compressNode = function(node) { return Graph.compressNode(node); }; /** * Returns a base64 encoded version of the compressed string. */ Graph.prototype.compress = function(data, deflate) { return Graph.compress(data, deflate); }; /** * Returns a decompressed version of the base64 encoded string. */ Graph.prototype.decompress = function(data, inflate) { return Graph.decompress(data, inflate); }; /** * Redirects to Graph.zapGremlins. */ Graph.prototype.zapGremlins = function(text) { return Graph.zapGremlins(text); }; /** * Hover icons are used for hover, vertex handler and drag from sidebar. */ HoverIcons = function(graph) { this.graph = graph; this.init(); }; /** * Up arrow. */ HoverIcons.prototype.arrowSpacing = 2; /** * Delay to switch to another state for overlapping bbox. Default is 500ms. */ HoverIcons.prototype.updateDelay = 500; /** * Delay to switch between states. Default is 140ms. */ HoverIcons.prototype.activationDelay = 140; /** * Up arrow. */ HoverIcons.prototype.currentState = null; /** * Up arrow. */ HoverIcons.prototype.activeArrow = null; /** * Up arrow. */ HoverIcons.prototype.inactiveOpacity = 15; /** * Up arrow. */ HoverIcons.prototype.cssCursor = 'copy'; /** * Whether to hide arrows that collide with vertices. * LATER: Add keyboard override, touch support. */ HoverIcons.prototype.checkCollisions = true; /** * Up arrow. */ HoverIcons.prototype.arrowFill = '#29b6f2'; /** * Up arrow. */ HoverIcons.prototype.triangleUp = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-up.png', 26, 14) : Graph.createSvgImage(18, 28, ''); /** * Right arrow. */ HoverIcons.prototype.triangleRight = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-right.png', 14, 26) : Graph.createSvgImage(26, 18, ''); /** * Down arrow. */ HoverIcons.prototype.triangleDown = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-down.png', 26, 14) : Graph.createSvgImage(18, 26, ''); /** * Left arrow. */ HoverIcons.prototype.triangleLeft = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-left.png', 14, 26) : Graph.createSvgImage(28, 18, ''); /** * Round target. */ HoverIcons.prototype.roundDrop = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/round-drop.png', 26, 26) : Graph.createSvgImage(26, 26, ''); /** * Refresh target. */ HoverIcons.prototype.refreshTarget = new mxImage((mxClient.IS_SVG) ? 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjM2cHgiIGhlaWdodD0iMzZweCI+PGVsbGlwc2UgZmlsbD0iIzI5YjZmMiIgY3g9IjEyIiBjeT0iMTIiIHJ4PSIxMiIgcnk9IjEyIi8+PHBhdGggdHJhbnNmb3JtPSJzY2FsZSgwLjgpIHRyYW5zbGF0ZSgyLjQsIDIuNCkiIHN0cm9rZT0iI2ZmZiIgZmlsbD0iI2ZmZiIgZD0iTTEyIDZ2M2w0LTQtNC00djNjLTQuNDIgMC04IDMuNTgtOCA4IDAgMS41Ny40NiAzLjAzIDEuMjQgNC4yNkw2LjcgMTQuOGMtLjQ1LS44My0uNy0xLjc5LS43LTIuOCAwLTMuMzEgMi42OS02IDYtNnptNi43NiAxLjc0TDE3LjMgOS4yYy40NC44NC43IDEuNzkuNyAyLjggMCAzLjMxLTIuNjkgNi02IDZ2LTNsLTQgNCA0IDR2LTNjNC40MiAwIDgtMy41OCA4LTggMC0xLjU3LS40Ni0zLjAzLTEuMjQtNC4yNnoiLz48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PC9zdmc+Cg==' : IMAGE_PATH + '/refresh.png', 38, 38); /** * Tolerance for hover icon clicks. */ HoverIcons.prototype.tolerance = (mxClient.IS_TOUCH) ? 6 : 0; /** * */ HoverIcons.prototype.init = function() { this.arrowUp = this.createArrow(this.triangleUp, mxResources.get('plusTooltip')); this.arrowRight = this.createArrow(this.triangleRight, mxResources.get('plusTooltip')); this.arrowDown = this.createArrow(this.triangleDown, mxResources.get('plusTooltip')); this.arrowLeft = this.createArrow(this.triangleLeft, mxResources.get('plusTooltip')); this.elts = [this.arrowUp, this.arrowRight, this.arrowDown, this.arrowLeft]; this.resetHandler = mxUtils.bind(this, function() { this.reset(); }); this.repaintHandler = mxUtils.bind(this, function() { this.repaint(); }); this.graph.selectionModel.addListener(mxEvent.CHANGE, this.resetHandler); this.graph.model.addListener(mxEvent.CHANGE, this.repaintHandler); this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler); this.graph.view.addListener(mxEvent.TRANSLATE, this.repaintHandler); this.graph.view.addListener(mxEvent.SCALE, this.repaintHandler); this.graph.view.addListener(mxEvent.DOWN, this.repaintHandler); this.graph.view.addListener(mxEvent.UP, this.repaintHandler); this.graph.addListener(mxEvent.ROOT, this.repaintHandler); this.graph.addListener(mxEvent.ESCAPE, this.resetHandler); mxEvent.addListener(this.graph.container, 'scroll', this.resetHandler); // Resets the mouse point on escape this.graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function() { this.mouseDownPoint = null; })); // Removes hover icons if mouse leaves the container mxEvent.addListener(this.graph.container, 'mouseleave', mxUtils.bind(this, function(evt) { // Workaround for IE11 firing mouseleave for touch in diagram if (evt.relatedTarget != null && mxEvent.getSource(evt) == this.graph.container) { this.setDisplay('none'); } })); // Resets current state when in-place editor starts this.graph.addListener(mxEvent.START_EDITING, mxUtils.bind(this, function(evt) { this.reset(); })); // Resets current state after update of selection state for touch events var graphClick = this.graph.click; this.graph.click = mxUtils.bind(this, function(me) { graphClick.apply(this.graph, arguments); if (this.currentState != null && !this.graph.isCellSelected(this.currentState.cell) && mxEvent.isTouchEvent(me.getEvent()) && !this.graph.model.isVertex(me.getCell())) { this.reset(); } }); // Checks if connection handler was active in mouse move // as workaround for possible double connection inserted var connectionHandlerActive = false; // Implements a listener for hover and click handling this.graph.addMouseListener( { mouseDown: mxUtils.bind(this, function(sender, me) { connectionHandlerActive = false; var evt = me.getEvent(); if (this.isResetEvent(evt)) { this.reset(); } else if (!this.isActive()) { var state = this.getState(me.getState()); if (state != null || !mxEvent.isTouchEvent(evt)) { this.update(state); } } this.setDisplay('none'); }), mouseMove: mxUtils.bind(this, function(sender, me) { var evt = me.getEvent(); if (this.isResetEvent(evt)) { this.reset(); } else if (!this.graph.isMouseDown && !mxEvent.isTouchEvent(evt)) { this.update(this.getState(me.getState()), me.getGraphX(), me.getGraphY()); } if (this.graph.connectionHandler != null && this.graph.connectionHandler.shape != null) { connectionHandlerActive = true; } }), mouseUp: mxUtils.bind(this, function(sender, me) { var evt = me.getEvent(); var pt = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)) if (this.isResetEvent(evt)) { this.reset(); } else if (this.isActive() && !connectionHandlerActive && this.mouseDownPoint != null) { this.click(this.currentState, this.getDirection(), me); } else if (this.isActive()) { // Selects target vertex after drag and clone if not only new edge was inserted if (this.graph.getSelectionCount() != 1 || !this.graph.model.isEdge( this.graph.getSelectionCell())) { this.update(this.getState(this.graph.view.getState( this.graph.getCellAt(me.getGraphX(), me.getGraphY())))); } else { this.reset(); } } else if (mxEvent.isTouchEvent(evt) || (this.bbox != null && mxUtils.contains(this.bbox, me.getGraphX(), me.getGraphY()))) { // Shows existing hover icons if inside bounding box this.setDisplay(''); this.repaint(); } else if (!mxEvent.isTouchEvent(evt)) { this.reset(); } connectionHandlerActive = false; this.resetActiveArrow(); }) }); }; /** * */ HoverIcons.prototype.isResetEvent = function(evt, allowShift) { return mxEvent.isAltDown(evt) || (this.activeArrow == null && mxEvent.isShiftDown(evt)) || mxEvent.isMetaDown(evt) || (mxEvent.isPopupTrigger(evt) && !mxEvent.isControlDown(evt)); }; /** * */ HoverIcons.prototype.createArrow = function(img, tooltip) { var arrow = null; if (mxClient.IS_IE && !mxClient.IS_SVG) { // Workaround for PNG images in IE6 if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') { arrow = document.createElement(mxClient.VML_PREFIX + ':image'); arrow.setAttribute('src', img.src); arrow.style.borderStyle = 'none'; } else { arrow = document.createElement('div'); arrow.style.backgroundImage = 'url(' + img.src + ')'; arrow.style.backgroundPosition = 'center'; arrow.style.backgroundRepeat = 'no-repeat'; } arrow.style.width = (img.width + 4) + 'px'; arrow.style.height = (img.height + 4) + 'px'; arrow.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; } else { arrow = mxUtils.createImage(img.src); arrow.style.width = img.width + 'px'; arrow.style.height = img.height + 'px'; arrow.style.padding = this.tolerance + 'px'; } if (tooltip != null) { arrow.setAttribute('title', tooltip); } arrow.style.position = 'absolute'; arrow.style.cursor = this.cssCursor; mxEvent.addGestureListeners(arrow, mxUtils.bind(this, function(evt) { if (this.currentState != null && !this.isResetEvent(evt)) { this.mouseDownPoint = mxUtils.convertPoint(this.graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); this.drag(evt, this.mouseDownPoint.x, this.mouseDownPoint.y); this.activeArrow = arrow; this.setDisplay('none'); mxEvent.consume(evt); } })); // Captures mouse events as events on graph mxEvent.redirectMouseEvents(arrow, this.graph, this.currentState); mxEvent.addListener(arrow, 'mouseenter', mxUtils.bind(this, function(evt) { // Workaround for Firefox firing mouseenter on touchend if (mxEvent.isMouseEvent(evt)) { if (this.activeArrow != null && this.activeArrow != arrow) { mxUtils.setOpacity(this.activeArrow, this.inactiveOpacity); } this.graph.connectionHandler.constraintHandler.reset(); mxUtils.setOpacity(arrow, 100); this.activeArrow = arrow; } })); mxEvent.addListener(arrow, 'mouseleave', mxUtils.bind(this, function(evt) { // Workaround for IE11 firing this event on touch if (!this.graph.isMouseDown) { this.resetActiveArrow(); } })); return arrow; }; /** * */ HoverIcons.prototype.resetActiveArrow = function() { if (this.activeArrow != null) { mxUtils.setOpacity(this.activeArrow, this.inactiveOpacity); this.activeArrow = null; } }; /** * */ HoverIcons.prototype.getDirection = function() { var dir = mxConstants.DIRECTION_EAST; if (this.activeArrow == this.arrowUp) { dir = mxConstants.DIRECTION_NORTH; } else if (this.activeArrow == this.arrowDown) { dir = mxConstants.DIRECTION_SOUTH; } else if (this.activeArrow == this.arrowLeft) { dir = mxConstants.DIRECTION_WEST; } return dir; }; /** * */ HoverIcons.prototype.visitNodes = function(visitor) { for (var i = 0; i < this.elts.length; i++) { if (this.elts[i] != null) { visitor(this.elts[i]); } } }; /** * */ HoverIcons.prototype.removeNodes = function() { this.visitNodes(function(elt) { if (elt.parentNode != null) { elt.parentNode.removeChild(elt); } }); }; /** * */ HoverIcons.prototype.setDisplay = function(display) { this.visitNodes(function(elt) { elt.style.display = display; }); }; /** * */ HoverIcons.prototype.isActive = function() { return this.activeArrow != null && this.currentState != null; }; /** * */ HoverIcons.prototype.drag = function(evt, x, y) { this.graph.popupMenuHandler.hideMenu(); this.graph.stopEditing(false); // Checks if state was removed in call to stopEditing above if (this.currentState != null) { this.graph.connectionHandler.start(this.currentState, x, y); this.graph.isMouseTrigger = mxEvent.isMouseEvent(evt); this.graph.isMouseDown = true; // Hides handles for selection cell var handler = this.graph.selectionCellsHandler.getHandler(this.currentState.cell); if (handler != null) { handler.setHandlesVisible(false); } // Ctrl+shift drag sets source constraint var es = this.graph.connectionHandler.edgeState; if (evt != null && mxEvent.isShiftDown(evt) && mxEvent.isControlDown(evt) && es != null && mxUtils.getValue(es.style, mxConstants.STYLE_EDGE, null) === 'orthogonalEdgeStyle') { var direction = this.getDirection(); es.cell.style = mxUtils.setStyle(es.cell.style, 'sourcePortConstraint', direction); es.style['sourcePortConstraint'] = direction; } } }; /** * */ HoverIcons.prototype.getStateAt = function(state, x, y) { return this.graph.view.getState(this.graph.getCellAt(x, y)); }; /** * */ HoverIcons.prototype.click = function(state, dir, me) { var evt = me.getEvent(); var x = me.getGraphX(); var y = me.getGraphY(); var tmp = this.getStateAt(state, x, y); if (tmp != null && this.graph.model.isEdge(tmp.cell) && !mxEvent.isControlDown(evt) && (tmp.getVisibleTerminalState(true) == state || tmp.getVisibleTerminalState(false) == state)) { this.graph.setSelectionCell(tmp.cell); this.reset(); } else if (state != null) { this.graph.selectCellsForConnectVertex(this.graph.connectVertex( state.cell, dir, this.graph.defaultEdgeLength, evt), evt, this); } me.consume(); }; /** * */ HoverIcons.prototype.reset = function(clearTimeout) { clearTimeout = (clearTimeout == null) ? true : clearTimeout; if (clearTimeout && this.updateThread != null) { window.clearTimeout(this.updateThread); } this.mouseDownPoint = null; this.currentState = null; this.activeArrow = null; this.removeNodes(); this.bbox = null; }; /** * */ HoverIcons.prototype.repaint = function() { this.bbox = null; if (this.currentState != null) { // Checks if cell was deleted this.currentState = this.getState(this.currentState); // Cell was deleted if (this.currentState != null && this.graph.model.isVertex(this.currentState.cell) && this.graph.isCellConnectable(this.currentState.cell)) { var bds = mxRectangle.fromRectangle(this.currentState); // Uses outer bounding box to take rotation into account if (this.currentState.shape != null && this.currentState.shape.boundingBox != null) { bds = mxRectangle.fromRectangle(this.currentState.shape.boundingBox); } bds.grow(this.graph.tolerance); bds.grow(this.arrowSpacing); var handler = this.graph.selectionCellsHandler.getHandler(this.currentState.cell); var rotationBbox = null; if (handler != null) { bds.x -= handler.horizontalOffset / 2; bds.y -= handler.verticalOffset / 2; bds.width += handler.horizontalOffset; bds.height += handler.verticalOffset; // Adds bounding box of rotation handle to avoid overlap if (handler.rotationShape != null && handler.rotationShape.node != null && handler.rotationShape.node.style.visibility != 'hidden' && handler.rotationShape.node.style.display != 'none' && handler.rotationShape.boundingBox != null) { rotationBbox = handler.rotationShape.boundingBox; } } // Positions arrows avoid collisions with rotation handle var positionArrow = mxUtils.bind(this, function(arrow, x, y) { if (rotationBbox != null) { var bbox = new mxRectangle(x, y, arrow.clientWidth, arrow.clientHeight); if (mxUtils.intersects(bbox, rotationBbox)) { if (arrow == this.arrowUp) { y -= bbox.y + bbox.height - rotationBbox.y; } else if (arrow == this.arrowRight) { x += rotationBbox.x + rotationBbox.width - bbox.x; } else if (arrow == this.arrowDown) { y += rotationBbox.y + rotationBbox.height - bbox.y; } else if (arrow == this.arrowLeft) { x -= bbox.x + bbox.width - rotationBbox.x; } } } arrow.style.left = x + 'px'; arrow.style.top = y + 'px'; mxUtils.setOpacity(arrow, this.inactiveOpacity); }); positionArrow(this.arrowUp, Math.round(this.currentState.getCenterX() - this.triangleUp.width / 2 - this.tolerance), Math.round(bds.y - this.triangleUp.height - this.tolerance)); positionArrow(this.arrowRight, Math.round(bds.x + bds.width - this.tolerance), Math.round(this.currentState.getCenterY() - this.triangleRight.height / 2 - this.tolerance)); positionArrow(this.arrowDown, parseInt(this.arrowUp.style.left), Math.round(bds.y + bds.height - this.tolerance)); positionArrow(this.arrowLeft, Math.round(bds.x - this.triangleLeft.width - this.tolerance), parseInt(this.arrowRight.style.top)); if (this.checkCollisions) { var right = this.graph.getCellAt(bds.x + bds.width + this.triangleRight.width / 2, this.currentState.getCenterY()); var left = this.graph.getCellAt(bds.x - this.triangleLeft.width / 2, this.currentState.getCenterY()); var top = this.graph.getCellAt(this.currentState.getCenterX(), bds.y - this.triangleUp.height / 2); var bottom = this.graph.getCellAt(this.currentState.getCenterX(), bds.y + bds.height + this.triangleDown.height / 2); // Shows hover icons large cell is behind all directions of current cell if (right != null && right == left && left == top && top == bottom) { right = null; left = null; top = null; bottom = null; } var currentGeo = this.graph.getCellGeometry(this.currentState.cell); var checkCollision = mxUtils.bind(this, function(cell, arrow) { var geo = this.graph.model.isVertex(cell) && this.graph.getCellGeometry(cell); // Ignores collision if vertex is more than 3 times the size of this vertex if (cell != null && !this.graph.model.isAncestor(cell, this.currentState.cell) && !this.graph.isSwimlane(cell) && (geo == null || currentGeo == null || (geo.height < 3 * currentGeo.height && geo.width < 3 * currentGeo.width))) { arrow.style.visibility = 'hidden'; } else { arrow.style.visibility = 'visible'; } }); checkCollision(right, this.arrowRight); checkCollision(left, this.arrowLeft); checkCollision(top, this.arrowUp); checkCollision(bottom, this.arrowDown); } else { this.arrowLeft.style.visibility = 'visible'; this.arrowRight.style.visibility = 'visible'; this.arrowUp.style.visibility = 'visible'; this.arrowDown.style.visibility = 'visible'; } if (this.graph.tooltipHandler.isEnabled()) { this.arrowLeft.setAttribute('title', mxResources.get('plusTooltip')); this.arrowRight.setAttribute('title', mxResources.get('plusTooltip')); this.arrowUp.setAttribute('title', mxResources.get('plusTooltip')); this.arrowDown.setAttribute('title', mxResources.get('plusTooltip')); } else { this.arrowLeft.removeAttribute('title'); this.arrowRight.removeAttribute('title'); this.arrowUp.removeAttribute('title'); this.arrowDown.removeAttribute('title'); } } else { this.reset(); } // Updates bounding box if (this.currentState != null) { this.bbox = this.computeBoundingBox(); // Adds tolerance for hover if (this.bbox != null) { this.bbox.grow(10); } } } }; /** * */ HoverIcons.prototype.computeBoundingBox = function() { var bbox = (!this.graph.model.isEdge(this.currentState.cell)) ? mxRectangle.fromRectangle(this.currentState) : null; this.visitNodes(function(elt) { if (elt.parentNode != null) { var tmp = new mxRectangle(elt.offsetLeft, elt.offsetTop, elt.offsetWidth, elt.offsetHeight); if (bbox == null) { bbox = tmp; } else { bbox.add(tmp); } } }); return bbox; }; /** * */ HoverIcons.prototype.getState = function(state) { if (state != null) { var cell = state.cell; if (!this.graph.getModel().contains(cell)) { state = null; } else { // Uses connectable parent vertex if child is not connectable if (this.graph.getModel().isVertex(cell) && !this.graph.isCellConnectable(cell)) { var parent = this.graph.getModel().getParent(cell); if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) { cell = parent; } } // Ignores locked cells and edges if (this.graph.isCellLocked(cell) || this.graph.model.isEdge(cell)) { cell = null; } state = this.graph.view.getState(cell); if (state != null && state.style == null) { state = null; } } } return state; }; /** * */ HoverIcons.prototype.update = function(state, x, y) { if (!this.graph.connectionArrowsEnabled || (state != null && mxUtils.getValue(state.style, 'allowArrows', '1') == '0')) { this.reset(); } else { if (state != null && state.cell.geometry != null && state.cell.geometry.relative && this.graph.model.isEdge(state.cell.parent)) { state = null; } var timeOnTarget = null; // Time on target if (this.prev != state || this.isActive()) { this.startTime = new Date().getTime(); this.prev = state; timeOnTarget = 0; if (this.updateThread != null) { window.clearTimeout(this.updateThread); } if (state != null) { // Starts timer to update current state with no mouse events this.updateThread = window.setTimeout(mxUtils.bind(this, function() { if (!this.isActive() && !this.graph.isMouseDown && !this.graph.panningHandler.isActive()) { this.prev = state; this.update(state, x, y); } }), this.updateDelay + 10); } } else if (this.startTime != null) { timeOnTarget = new Date().getTime() - this.startTime; } this.setDisplay(''); if (this.currentState != null && this.currentState != state && timeOnTarget < this.activationDelay && this.bbox != null && !mxUtils.contains(this.bbox, x, y)) { this.reset(false); } else if (this.currentState != null || timeOnTarget > this.activationDelay) { if (this.currentState != state && ((timeOnTarget > this.updateDelay && state != null) || this.bbox == null || x == null || y == null || !mxUtils.contains(this.bbox, x, y))) { if (state != null && this.graph.isEnabled()) { this.removeNodes(); this.setCurrentState(state); this.repaint(); // Resets connection points on other focused cells if (this.graph.connectionHandler.constraintHandler.currentFocus != state) { this.graph.connectionHandler.constraintHandler.reset(); } } else { this.reset(); } } } } }; /** * */ HoverIcons.prototype.setCurrentState = function(state) { if (state.style['portConstraint'] != 'eastwest') { this.graph.container.appendChild(this.arrowUp); this.graph.container.appendChild(this.arrowDown); } this.graph.container.appendChild(this.arrowRight); this.graph.container.appendChild(this.arrowLeft); this.currentState = state; }; /** * Returns true if the given cell is a table. */ Graph.prototype.createParent = function(parent, child, childCount) { parent = this.cloneCell(parent); for (var i = 0; i < childCount; i++) { parent.insert(this.cloneCell(child)); } return parent; }; /** * Returns true if the given cell is a table. */ Graph.prototype.createTable = function(rowCount, colCount, w, h) { w = (w != null) ? w : 40; h = (h != null) ? h : 30; return this.createParent(this.createVertex(null, null, '', 0, 0, colCount * w, rowCount * h, 'html=1;whiteSpace=wrap;container=1;collapsible=0;childLayout=tableLayout;'), this.createParent(this.createVertex(null, null, '', 0, 0, colCount * w, h, 'html=1;whiteSpace=wrap;container=1;collapsible=0;points=[[0,0.5],[1,0.5]];part=1;'), this.createVertex(null, null, '', 0, 0, w, h, 'html=1;whiteSpace=wrap;connectable=0;part=1;'), colCount), rowCount); }; /** * */ Graph.prototype.createCrossFunctionalSwimlane = function(rowCount, colCount, w, h) { w = (w != null) ? w : 120; h = (h != null) ? h : 120; var s = 'swimlane;html=1;whiteSpace=wrap;container=1;' + 'collapsible=0;recursiveResize=0;expand=0;'; var table = this.createVertex(null, null, '', 0, 0, colCount * w, rowCount * h, s + 'childLayout=tableLayout;'); var row = this.createVertex(null, null, '', 0, 0, colCount * w, h, s + 'horizontal=0;points=[[0,0.5],[1,0.5]];part=1;'); table.insert(this.createParent(row, this.createVertex(null, null, '', 0, 0, w, h, s + 'connectable=0;part=1;'), colCount)); if (rowCount > 1) { return this.createParent(table, this.createParent(row, this.createVertex(null, null, '', 0, 0, w, h, s + 'connectable=0;part=1;startSize=0;'), colCount), rowCount - 1); } else { return table; } }; /** * Returns true if the given cell is a table cell. */ Graph.prototype.isTableCell = function(cell) { return this.isTableRow(this.model.getParent(cell)); }; /** * Returns true if the given cell is a table row. */ Graph.prototype.isTableRow = function(cell) { return this.isTable(this.model.getParent(cell)); }; /** * Returns true if the given cell is a table. */ Graph.prototype.isTable = function(cell) { var style = this.getCellStyle(cell); return style != null && style['childLayout'] == 'tableLayout'; }; /** * Updates column width and row height. */ Graph.prototype.getActualStartSize = function(swimlane, ignoreState) { var result = new mxRectangle(); if (this.isSwimlane(swimlane)) { var style = this.getCurrentCellStyle(swimlane, ignoreState); var size = parseInt(mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); var flipH = mxUtils.getValue(style, mxConstants.STYLE_FLIPH, 0) == 1; var flipV = mxUtils.getValue(style, mxConstants.STYLE_FLIPV, 0) == 1; var h = mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true); var n = 0; if (!h) { n++; } var dir = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST); if (dir == mxConstants.DIRECTION_NORTH) { n++; } else if (dir == mxConstants.DIRECTION_WEST) { n += 2; } else if (dir == mxConstants.DIRECTION_SOUTH) { n += 3; } n = mxUtils.mod(n, 4); if (n == 0) { result.y = size; } else if (n == 1) { result.x = size; } else if (n == 2) { result.height = size; } else if (n == 3) { result.width = size; } if (flipV) { var tmp = result.y; result.y = result.height; result.height = tmp; } if (flipH) { var tmp = result.x; result.x = result.width; result.width = tmp; } } return result; }; /** * Updates column width and row height. */ Graph.prototype.tableResized = function(table) { console.log('tableLayout.tableResized', table); var model = this.getModel(); var rowCount = model.getChildCount(table); var tableGeo = this.getCellGeometry(table); if (tableGeo != null && rowCount > 0) { var off = this.getActualStartSize(table); var y = off.y; for (var i = 0; i < rowCount; i++) { var row = model.getChildAt(table, i); if (row != null && model.isVertex(row)) { var rowGeo = this.getCellGeometry(row); if (rowGeo != null) { if (i == rowCount - 1) { var newRowGeo = rowGeo.clone(); newRowGeo.width = tableGeo.width - off.x; if (y < tableGeo.height) { newRowGeo.height = tableGeo.height - y; } else if (y > tableGeo.height) { tableGeo.height = y + Graph.minTableRowHeight; newRowGeo.height = Graph.minTableRowHeight; } model.setGeometry(row, newRowGeo); this.tableRowResized(row, newRowGeo, rowGeo); } y += rowGeo.height; } } } } }; /** * Updates column width and row height. */ Graph.prototype.setRowHeight = function(row, height) { var model = this.getModel(); model.beginUpdate(); try { for (var i = 0; i < model.getChildCount(row); i++) { var child = model.getChildAt(row, i); if (model.isVertex(child)) { var childGeo = this.getCellGeometry(child); if (childGeo != null) { childGeo = childGeo.clone(); childGeo.height = height; model.setGeometry(child, childGeo); } } } } finally { model.endUpdate(); } }; /** * Updates column width and row height. */ Graph.prototype.tableRowResized = function(row, bounds, prev) { console.log('tableLayout.tableRowResized', row); var model = this.getModel(); var rowGeo = this.getCellGeometry(row); var cellCount = model.getChildCount(row); if (rowGeo != null && cellCount > 0) { var off = this.getActualStartSize(row); var x = off.x; for (var i = 0; i < cellCount; i++) { var cell = model.getChildAt(row, i); if (cell != null && model.isVertex(cell)) { var geo = this.getCellGeometry(cell); if (geo != null) { var newGeo = geo.clone(); newGeo.height = rowGeo.height - off.y; model.setGeometry(cell, newGeo); if (i == cellCount - 1) { if (x < rowGeo.width) { newGeo.width = rowGeo.width - x; } else if (x > rowGeo.width) { rowGeo.width = x + Graph.minTableColumnWidth; newGeo.width = Graph.minTableColumnWidth; } this.tableCellResized(cell, newGeo, geo); } x += geo.width; } } } } // Updates previous row height if upper edge was moved var table = model.getParent(row); var index = table.getIndex(row); if (bounds.y != prev.y && index > 0) { var previousRow = model.getChildAt(table, index - 1); var prg = this.getCellGeometry(previousRow); if (prg != null) { prg = prg.clone(); prg.height -= prev.y - bounds.y; model.setGeometry(previousRow, prg); } } }; /** * Updates column width and row height. */ Graph.prototype.tableCellResized = function(cell, bounds, prev) { console.log('tableLayout.tableCellResized', cell, bounds, prev); var geo = this.getCellGeometry(cell); if (geo != null) { var model = this.getModel(); var row = model.getParent(cell); var table = model.getParent(row); var index = row.getIndex(cell); // Applies new height to all cells in the row if (bounds.height != prev.height) { this.setRowHeight(row, geo.height); } // Updates column width var previousRow = null; for (var i = 0; i < model.getChildCount(table); i++) { var currentRow = model.getChildAt(table, i); if (model.isVertex(currentRow)) { var child = model.getChildAt(currentRow, index); if (cell != child) { var childGeo = this.getCellGeometry(child); if (childGeo != null) { childGeo = childGeo.clone(); childGeo.width = geo.width; model.setGeometry(child, childGeo); } } // Updates previous row height if (bounds.y != prev.y && currentRow == row && previousRow != null) { var prg = this.getCellGeometry(previousRow); if (prg != null) { this.setRowHeight(previousRow, prg.height - prev.y + bounds.y); } } previousRow = currentRow; } } // Updates previous column width if (bounds.x != prev.x && index > 0) { var child = model.getChildAt(row, index - 1); var childGeo = this.getCellGeometry(child); if (childGeo != null) { var newChildGeo = childGeo.clone(); newChildGeo.width -= prev.x - bounds.x; model.setGeometry(child, newChildGeo); this.tableCellResized(child, newChildGeo, childGeo); } } } }; /** * Table Layout */ function TableLayout(graph) { mxGraphLayout.call(this, graph); }; /** * Extends mxGraphLayout */ TableLayout.prototype = new mxGraphLayout(); TableLayout.prototype.constructor = TableLayout; /** * Reorders rows. */ TableLayout.prototype.moveCell = function(cell, x, y) { // TODO: Reorder rows console.log('tableLayout.moveCell', cell, x, y); }; /** * */ TableLayout.prototype.resizeCell = function(cell, geo, prev) { if (this.graph.isTable(cell)) { this.graph.tableResized(cell, geo, prev); } else if (this.graph.isTableRow(cell)) { this.graph.tableRowResized(cell, geo, prev); } else if (this.graph.isTableCell(cell)) { this.graph.tableCellResized(cell, geo, prev); } }; /** * Updates column width and row height. */ TableLayout.prototype.execute = function(table) { var off = this.graph.getActualStartSize(table, true); var model = this.graph.getModel(); var y = off.y; var rows = []; var maxX = 0; var x = 0; console.log('tableLayout.execute', table, off); for (var i = 0; i < model.getChildCount(table); i++) { var row = model.getChildAt(table, i); if (row != null && model.isVertex(row)) { var rowGeo = this.graph.getCellGeometry(row); if (rowGeo != null) { rowGeo = rowGeo.clone(); var rowOff = this.graph.getActualStartSize(row, true); var childCount = model.getChildCount(row); x = rowOff.x; for (var j = 0; j < childCount; j++) { var cell = model.getChildAt(row, j); if (cell != null) { var geo = this.graph.getCellGeometry(cell); if (geo != null) { geo = geo.clone(); rowGeo.height = geo.height + rowOff.y + rowOff.height; geo.x = x; geo.y = rowOff.y; model.setGeometry(cell, geo); x += geo.width; if (j == childCount - 1) { rows.push([geo, rowGeo, rowOff]); maxX = Math.max(x, maxX); } } } } rowGeo.width = x + rowOff.width; rowGeo.y = y; rowGeo.x = off.x; model.setGeometry(row, rowGeo); y += rowGeo.height; } } } // Updates table size var tableGeo = this.graph.getCellGeometry(table); if (tableGeo != null) { tableGeo = tableGeo.clone(); tableGeo.width = x + off.x + off.width; tableGeo.height = y + off.height; model.setGeometry(table, tableGeo); for (var i = 0; i < rows.length; i++) { if (rows[i][1].width < maxX) { rows[i][0].width += maxX - rows[i][1].width; rows[i][1].width += maxX - rows[i][1].width; } } } }; /** * Table Layout */ function TableRowLayout(graph) { mxGraphLayout.call(this, graph); }; /** * Extends mxGraphLayout */ TableRowLayout.prototype = new TableLayout(); TableRowLayout.prototype.constructor = TableRowLayout; /** * Reorders rows. */ TableRowLayout.prototype.moveCell = function(cell, x, y) { // TODO: Reorder columns console.log('TableRowLayout.moveCell', cell, x, y); }; /** * Updates row start sizes. */ TableRowLayout.prototype.execute = function(row) { var off = this.graph.getActualStartSize(row, true); var off0 = this.graph.getActualStartSize(row); var style = this.graph.getCellStyle(row); var model = this.graph.getModel(); var table = model.getParent(row); console.log('tableRowLayout.execute', row, off, off0); if (style != null && table != null) { var size = parseInt(mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); model.beginUpdate(); try { // Swimlane rotation requires resize of last row element if (this.graph.isTableRow(row)) { if (off.width != off0.width || off.x != off0.x) { var cell = model.getChildAt(row, model.getChildCount(row) - 1); var geo = this.graph.getCellGeometry(cell); if (geo != null) { geo = geo.clone(); geo.width += off0.width - off.width - off.x - off0.x; geo.width = Math.max(Graph.minTableColumnWidth, geo.width); model.setGeometry(cell, geo); } } } for (var i = 0; i < model.getChildCount(table); i++) { var current = model.getChildAt(table, i); if (current != row) { var temp = this.graph.getActualStartSize(current); // Checks if same side is offset if ((off.x > 0 && temp.x > 0) || (off.y > 0 && temp.y > 0) || (off.width > 0 && temp.width > 0) || (off.height > 0 && temp.height > 0)) { this.graph.setCellStyles( mxConstants.STYLE_STARTSIZE, size, [current]); } } } } finally { model.endUpdate(); } } }; (function() { /** * Reset the list of processed edges. */ var mxGraphViewResetValidationState = mxGraphView.prototype.resetValidationState; mxGraphView.prototype.resetValidationState = function() { mxGraphViewResetValidationState.apply(this, arguments); this.validEdges = []; }; /** * Updates jumps for valid edges and repaints if needed. */ var mxGraphViewValidateCellState = mxGraphView.prototype.validateCellState; mxGraphView.prototype.validateCellState = function(cell, recurse) { recurse = (recurse != null) ? recurse : true; var state = this.getState(cell); // Forces repaint if jumps change on a valid edge if (state != null && recurse && this.graph.model.isEdge(state.cell) && state.style != null && state.style[mxConstants.STYLE_CURVED] != 1 && !state.invalid && this.updateLineJumps(state)) { this.graph.cellRenderer.redraw(state, false, this.isRendering()); } state = mxGraphViewValidateCellState.apply(this, arguments); // Adds to the list of edges that may intersect with later edges if (state != null && recurse && this.graph.model.isEdge(state.cell) && state.style != null && state.style[mxConstants.STYLE_CURVED] != 1) { // LATER: Reuse jumps for valid edges this.validEdges.push(state); } return state; }; /** * Forces repaint if routed points have changed. */ var mxCellRendererIsShapeInvalid = mxCellRenderer.prototype.isShapeInvalid; mxCellRenderer.prototype.isShapeInvalid = function(state, shape) { return mxCellRendererIsShapeInvalid.apply(this, arguments) || (state.routedPoints != null && shape.routedPoints != null && !mxUtils.equalPoints(shape.routedPoints, state.routedPoints)) }; /** * Updates jumps for invalid edges. */ var mxGraphViewUpdateCellState = mxGraphView.prototype.updateCellState; mxGraphView.prototype.updateCellState = function(state) { mxGraphViewUpdateCellState.apply(this, arguments); // Updates jumps on invalid edge before repaint if (this.graph.model.isEdge(state.cell) && state.style[mxConstants.STYLE_CURVED] != 1) { this.updateLineJumps(state); } }; /** * Updates the jumps between given state and processed edges. */ mxGraphView.prototype.updateLineJumps = function(state) { var pts = state.absolutePoints; if (Graph.lineJumpsEnabled) { var changed = state.routedPoints != null; var actual = null; if (pts != null && this.validEdges != null && mxUtils.getValue(state.style, 'jumpStyle', 'none') !== 'none') { var thresh = 0.5 * this.scale; changed = false; actual = []; // Type 0 means normal waypoint, 1 means jump function addPoint(type, x, y) { var rpt = new mxPoint(x, y); rpt.type = type; actual.push(rpt); var curr = (state.routedPoints != null) ? state.routedPoints[actual.length - 1] : null; return curr == null || curr.type != type || curr.x != x || curr.y != y; }; for (var i = 0; i < pts.length - 1; i++) { var p1 = pts[i + 1]; var p0 = pts[i]; var list = []; // Ignores waypoints on straight segments var pn = pts[i + 2]; while (i < pts.length - 2 && mxUtils.ptSegDistSq(p0.x, p0.y, pn.x, pn.y, p1.x, p1.y) < 1 * this.scale * this.scale) { p1 = pn; i++; pn = pts[i + 2]; } changed = addPoint(0, p0.x, p0.y) || changed; // Processes all previous edges for (var e = 0; e < this.validEdges.length; e++) { var state2 = this.validEdges[e]; var pts2 = state2.absolutePoints; if (pts2 != null && mxUtils.intersects(state, state2) && state2.style['noJump'] != '1') { // Compares each segment of the edge with the current segment for (var j = 0; j < pts2.length - 1; j++) { var p3 = pts2[j + 1]; var p2 = pts2[j]; // Ignores waypoints on straight segments pn = pts2[j + 2]; while (j < pts2.length - 2 && mxUtils.ptSegDistSq(p2.x, p2.y, pn.x, pn.y, p3.x, p3.y) < 1 * this.scale * this.scale) { p3 = pn; j++; pn = pts2[j + 2]; } var pt = mxUtils.intersection(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); // Handles intersection between two segments if (pt != null && (Math.abs(pt.x - p0.x) > thresh || Math.abs(pt.y - p0.y) > thresh) && (Math.abs(pt.x - p1.x) > thresh || Math.abs(pt.y - p1.y) > thresh) && (Math.abs(pt.x - p2.x) > thresh || Math.abs(pt.y - p2.y) > thresh) && (Math.abs(pt.x - p3.x) > thresh || Math.abs(pt.y - p3.y) > thresh)) { var dx = pt.x - p0.x; var dy = pt.y - p0.y; var temp = {distSq: dx * dx + dy * dy, x: pt.x, y: pt.y}; // Intersections must be ordered by distance from start of segment for (var t = 0; t < list.length; t++) { if (list[t].distSq > temp.distSq) { list.splice(t, 0, temp); temp = null; break; } } // Ignores multiple intersections at segment joint if (temp != null && (list.length == 0 || list[list.length - 1].x !== temp.x || list[list.length - 1].y !== temp.y)) { list.push(temp); } } } } } // Adds ordered intersections to routed points for (var j = 0; j < list.length; j++) { changed = addPoint(1, list[j].x, list[j].y) || changed; } } var pt = pts[pts.length - 1]; changed = addPoint(0, pt.x, pt.y) || changed; } state.routedPoints = actual; return changed; } else { return false; } }; /** * Overrides painting the actual shape for taking into account jump style. */ var mxConnectorPaintLine = mxConnector.prototype.paintLine; mxConnector.prototype.paintLine = function (c, absPts, rounded) { // Required for checking dirty state this.routedPoints = (this.state != null) ? this.state.routedPoints : null; if (this.outline || this.state == null || this.style == null || this.state.routedPoints == null || this.state.routedPoints.length == 0) { mxConnectorPaintLine.apply(this, arguments); } else { var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; var size = (parseInt(mxUtils.getValue(this.style, 'jumpSize', Graph.defaultJumpSize)) - 2) / 2 + this.strokewidth; var style = mxUtils.getValue(this.style, 'jumpStyle', 'none'); var moveTo = true; var last = null; var len = null; var pts = []; var n = null; c.begin(); for (var i = 0; i < this.state.routedPoints.length; i++) { var rpt = this.state.routedPoints[i]; var pt = new mxPoint(rpt.x / this.scale, rpt.y / this.scale); // Takes first and last point from passed-in array if (i == 0) { pt = absPts[0]; } else if (i == this.state.routedPoints.length - 1) { pt = absPts[absPts.length - 1]; } var done = false; // Type 1 is an intersection if (last != null && rpt.type == 1) { // Checks if next/previous points are too close var next = this.state.routedPoints[i + 1]; var dx = next.x / this.scale - pt.x; var dy = next.y / this.scale - pt.y; var dist = dx * dx + dy * dy; if (n == null) { n = new mxPoint(pt.x - last.x, pt.y - last.y); len = Math.sqrt(n.x * n.x + n.y * n.y); if (len > 0) { n.x = n.x * size / len; n.y = n.y * size / len; } else { n = null; } } if (dist > size * size && len > 0) { var dx = last.x - pt.x; var dy = last.y - pt.y; var dist = dx * dx + dy * dy; if (dist > size * size) { var p0 = new mxPoint(pt.x - n.x, pt.y - n.y); var p1 = new mxPoint(pt.x + n.x, pt.y + n.y); pts.push(p0); this.addPoints(c, pts, rounded, arcSize, false, null, moveTo); var f = (Math.round(n.x) < 0 || (Math.round(n.x) == 0 && Math.round(n.y) <= 0)) ? 1 : -1; moveTo = false; if (style == 'sharp') { c.lineTo(p0.x - n.y * f, p0.y + n.x * f); c.lineTo(p1.x - n.y * f, p1.y + n.x * f); c.lineTo(p1.x, p1.y); } else if (style == 'arc') { f *= 1.3; c.curveTo(p0.x - n.y * f, p0.y + n.x * f, p1.x - n.y * f, p1.y + n.x * f, p1.x, p1.y); } else { c.moveTo(p1.x, p1.y); moveTo = true; } pts = [p1]; done = true; } } } else { n = null; } if (!done) { pts.push(pt); last = pt; } } this.addPoints(c, pts, rounded, arcSize, false, null, moveTo); c.stroke(); } }; /** * Adds support for snapToPoint style. */ var mxGraphViewUpdateFloatingTerminalPoint = mxGraphView.prototype.updateFloatingTerminalPoint; mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source) { if (start != null && edge != null && (start.style['snapToPoint'] == '1' || edge.style['snapToPoint'] == '1')) { start = this.getTerminalPort(edge, start, source); var next = this.getNextPoint(edge, end, source); var orth = this.graph.isOrthogonal(edge); var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0')); var center = new mxPoint(start.getCenterX(), start.getCenterY()); if (alpha != 0) { var cos = Math.cos(-alpha); var sin = Math.sin(-alpha); next = mxUtils.getRotatedPoint(next, cos, sin, center); } var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); border += parseFloat(edge.style[(source) ? mxConstants.STYLE_SOURCE_PERIMETER_SPACING : mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0); var pt = this.getPerimeterPoint(start, next, alpha == 0 && orth, border); if (alpha != 0) { var cos = Math.cos(alpha); var sin = Math.sin(alpha); pt = mxUtils.getRotatedPoint(pt, cos, sin, center); } edge.setAbsoluteTerminalPoint(this.snapToAnchorPoint(edge, start, end, source, pt), source); } else { mxGraphViewUpdateFloatingTerminalPoint.apply(this, arguments); } }; mxGraphView.prototype.snapToAnchorPoint = function(edge, start, end, source, pt) { if (start != null && edge != null) { var constraints = this.graph.getAllConnectionConstraints(start) var nearest = null; var dist = null; if (constraints != null) { for (var i = 0; i < constraints.length; i++) { var cp = this.graph.getConnectionPoint(start, constraints[i]); if (cp != null) { var tmp = (cp.x - pt.x) * (cp.x - pt.x) + (cp.y - pt.y) * (cp.y - pt.y); if (dist == null || tmp < dist) { nearest = cp; dist = tmp; } } } } if (nearest != null) { pt = nearest; } } return pt; }; /** * Adds support for placeholders in text elements of shapes. */ var mxStencilEvaluateTextAttribute = mxStencil.prototype.evaluateTextAttribute; mxStencil.prototype.evaluateTextAttribute = function(node, attribute, shape) { var result = mxStencilEvaluateTextAttribute.apply(this, arguments); var placeholders = node.getAttribute('placeholders'); if (placeholders == '1' && shape.state != null) { result = shape.state.view.graph.replacePlaceholders(shape.state.cell, result); } return result; }; /** * Adds custom stencils defined via shape=stencil(value) style. The value is a base64 encoded, compressed and * URL encoded XML definition of the shape according to the stencil definition language of mxGraph. * * Needs to be in this file to make sure its part of the embed client code. Also the check for ZLib is * different than for the Editor code. */ var mxCellRendererCreateShape = mxCellRenderer.prototype.createShape; mxCellRenderer.prototype.createShape = function(state) { if (state.style != null && typeof(pako) !== 'undefined') { var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); // Extracts and decodes stencil XML if shape has the form shape=stencil(value) if (shape != null && typeof shape === 'string' && shape.substring(0, 8) == 'stencil(') { try { var stencil = shape.substring(8, shape.length - 1); var doc = mxUtils.parseXml(Graph.decompress(stencil)); return new mxShape(new mxStencil(doc.documentElement)); } catch (e) { if (window.console != null) { console.log('Error in shape: ' + e); } } } } return mxCellRendererCreateShape.apply(this, arguments); }; })(); /** * Overrides stencil registry for dynamic loading of stencils. */ /** * Maps from library names to an array of Javascript filenames, * which are synchronously loaded. Currently only stencil files * (.xml) and JS files (.js) are supported. * IMPORTANT: For embedded diagrams to work entries must also * be added in EmbedServlet.java. */ mxStencilRegistry.libraries = {}; /** * Global switch to disable dynamic loading. */ mxStencilRegistry.dynamicLoading = true; /** * Global switch to disable eval for JS (preload all JS instead). */ mxStencilRegistry.allowEval = true; /** * Stores all package names that have been dynamically loaded. * Each package is only loaded once. */ mxStencilRegistry.packages = []; // Extends the default stencil registry to add dynamic loading mxStencilRegistry.getStencil = function(name) { var result = mxStencilRegistry.stencils[name]; if (result == null && mxCellRenderer.defaultShapes[name] == null && mxStencilRegistry.dynamicLoading) { var basename = mxStencilRegistry.getBasenameForStencil(name); // Loads stencil files and tries again if (basename != null) { var libs = mxStencilRegistry.libraries[basename]; if (libs != null) { if (mxStencilRegistry.packages[basename] == null) { for (var i = 0; i < libs.length; i++) { var fname = libs[i]; if (fname.toLowerCase().substring(fname.length - 4, fname.length) == '.xml') { mxStencilRegistry.loadStencilSet(fname, null); } else if (fname.toLowerCase().substring(fname.length - 3, fname.length) == '.js') { try { if (mxStencilRegistry.allowEval) { var req = mxUtils.load(fname); if (req != null && req.getStatus() >= 200 && req.getStatus() <= 299) { eval.call(window, req.getText()); } } } catch (e) { if (window.console != null) { console.log('error in getStencil:', fname, e); } } } else { // FIXME: This does not yet work as the loading is triggered after // the shape was used in the graph, at which point the keys have // typically been translated in the calling method. //mxResources.add(fname); } } mxStencilRegistry.packages[basename] = 1; } } else { // Replaces '_-_' with '_' basename = basename.replace('_-_', '_'); mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/' + basename + '.xml', null); } result = mxStencilRegistry.stencils[name]; } } return result; }; // Returns the basename for the given stencil or null if no file must be // loaded to render the given stencil. mxStencilRegistry.getBasenameForStencil = function(name) { var tmp = null; if (name != null && typeof name === 'string') { var parts = name.split('.'); if (parts.length > 0 && parts[0] == 'mxgraph') { tmp = parts[1]; for (var i = 2; i < parts.length - 1; i++) { tmp += '/' + parts[i]; } } } return tmp; }; // Loads the given stencil set mxStencilRegistry.loadStencilSet = function(stencilFile, postStencilLoad, force, async) { force = (force != null) ? force : false; // Uses additional cache for detecting previous load attempts var xmlDoc = mxStencilRegistry.packages[stencilFile]; if (force || xmlDoc == null) { var install = false; if (xmlDoc == null) { try { if (async) { mxStencilRegistry.loadStencil(stencilFile, mxUtils.bind(this, function(xmlDoc2) { if (xmlDoc2 != null && xmlDoc2.documentElement != null) { mxStencilRegistry.packages[stencilFile] = xmlDoc2; install = true; mxStencilRegistry.parseStencilSet(xmlDoc2.documentElement, postStencilLoad, install); } })); return; } else { xmlDoc = mxStencilRegistry.loadStencil(stencilFile); mxStencilRegistry.packages[stencilFile] = xmlDoc; install = true; } } catch (e) { if (window.console != null) { console.log('error in loadStencilSet:', stencilFile, e); } } } if (xmlDoc != null && xmlDoc.documentElement != null) { mxStencilRegistry.parseStencilSet(xmlDoc.documentElement, postStencilLoad, install); } } }; // Loads the given stencil XML file. mxStencilRegistry.loadStencil = function(filename, fn) { if (fn != null) { var req = mxUtils.get(filename, mxUtils.bind(this, function(req) { fn((req.getStatus() >= 200 && req.getStatus() <= 299) ? req.getXml() : null); })); } else { return mxUtils.load(filename).getXml(); } }; // Takes array of strings mxStencilRegistry.parseStencilSets = function(stencils) { for (var i = 0; i < stencils.length; i++) { mxStencilRegistry.parseStencilSet(mxUtils.parseXml(stencils[i]).documentElement); } }; // Parses the given stencil set mxStencilRegistry.parseStencilSet = function(root, postStencilLoad, install) { if (root.nodeName == 'stencils') { var shapes = root.firstChild; while (shapes != null) { if (shapes.nodeName == 'shapes') { mxStencilRegistry.parseStencilSet(shapes, postStencilLoad, install); } shapes = shapes.nextSibling; } } else { install = (install != null) ? install : true; var shape = root.firstChild; var packageName = ''; var name = root.getAttribute('name'); if (name != null) { packageName = name + '.'; } while (shape != null) { if (shape.nodeType == mxConstants.NODETYPE_ELEMENT) { name = shape.getAttribute('name'); if (name != null) { packageName = packageName.toLowerCase(); var stencilName = name.replace(/ /g,"_"); if (install) { mxStencilRegistry.addStencil(packageName + stencilName.toLowerCase(), new mxStencil(shape)); } if (postStencilLoad != null) { var w = shape.getAttribute('w'); var h = shape.getAttribute('h'); w = (w == null) ? 80 : parseInt(w, 10); h = (h == null) ? 80 : parseInt(h, 10); postStencilLoad(packageName, stencilName, name, w, h); } } } shape = shape.nextSibling; } } }; /** * These overrides are only added if mxVertexHandler is defined (ie. not in embedded graph) */ if (typeof mxVertexHandler != 'undefined') { (function() { // Sets colors for handles mxConstants.HANDLE_FILLCOLOR = '#29b6f2'; mxConstants.HANDLE_STROKECOLOR = '#0088cf'; mxConstants.VERTEX_SELECTION_COLOR = '#00a8ff'; mxConstants.OUTLINE_COLOR = '#00a8ff'; mxConstants.OUTLINE_HANDLE_FILLCOLOR = '#99ccff'; mxConstants.OUTLINE_HANDLE_STROKECOLOR = '#00a8ff'; mxConstants.CONNECT_HANDLE_FILLCOLOR = '#cee7ff'; mxConstants.EDGE_SELECTION_COLOR = '#00a8ff'; mxConstants.DEFAULT_VALID_COLOR = '#00a8ff'; mxConstants.LABEL_HANDLE_FILLCOLOR = '#cee7ff'; mxConstants.GUIDE_COLOR = '#0088cf'; mxConstants.HIGHLIGHT_OPACITY = 30; mxConstants.HIGHLIGHT_SIZE = 5; // Enables snapping to off-grid terminals for edge waypoints mxEdgeHandler.prototype.snapToTerminals = true; // Enables guides mxGraphHandler.prototype.guidesEnabled = true; // Removes parents where all child cells are moved out mxGraphHandler.prototype.removeEmptyParents = true; // Enables fading of rubberband mxRubberband.prototype.fadeOut = true; // Alt-move disables guides mxGuide.prototype.isEnabledForEvent = function(evt) { return !mxEvent.isAltDown(evt); }; // Extends connection handler to enable ctrl+drag for cloning source cell // since copyOnConnect is now disabled by default var mxConnectionHandlerCreateTarget = mxConnectionHandler.prototype.isCreateTarget; mxConnectionHandler.prototype.isCreateTarget = function(evt) { return mxEvent.isControlDown(evt) || mxConnectionHandlerCreateTarget.apply(this, arguments); }; // Overrides highlight shape for connection points mxConstraintHandler.prototype.createHighlightShape = function() { var hl = new mxEllipse(null, this.highlightColor, this.highlightColor, 0); hl.opacity = mxConstants.HIGHLIGHT_OPACITY; return hl; }; // Overrides edge preview to use current edge shape and default style mxConnectionHandler.prototype.livePreview = true; mxConnectionHandler.prototype.cursor = 'crosshair'; // Uses current edge style for connect preview mxConnectionHandler.prototype.createEdgeState = function(me) { var style = this.graph.createCurrentEdgeStyle(); var edge = this.graph.createEdge(null, null, null, null, null, style); var state = new mxCellState(this.graph.view, edge, this.graph.getCellStyle(edge)); for (var key in this.graph.currentEdgeStyle) { state.style[key] = this.graph.currentEdgeStyle[key]; } return state; }; // Overrides dashed state with current edge style var connectionHandlerCreateShape = mxConnectionHandler.prototype.createShape; mxConnectionHandler.prototype.createShape = function() { var shape = connectionHandlerCreateShape.apply(this, arguments); shape.isDashed = this.graph.currentEdgeStyle[mxConstants.STYLE_DASHED] == '1'; return shape; } // Overrides live preview to keep current style mxConnectionHandler.prototype.updatePreview = function(valid) { // do not change color of preview }; // Overrides connection handler to ignore edges instead of not allowing connections var mxConnectionHandlerCreateMarker = mxConnectionHandler.prototype.createMarker; mxConnectionHandler.prototype.createMarker = function() { var marker = mxConnectionHandlerCreateMarker.apply(this, arguments); var markerGetCell = marker.getCell; marker.getCell = mxUtils.bind(this, function(me) { var result = markerGetCell.apply(this, arguments); this.error = null; return result; }); return marker; }; /** * */ Graph.prototype.defaultVertexStyle = {}; /** * Contains the default style for edges. */ Graph.prototype.defaultEdgeStyle = {'edgeStyle': 'orthogonalEdgeStyle', 'rounded': '0', 'jettySize': 'auto', 'orthogonalLoop': '1'}; /** * Returns the current edge style as a string. */ Graph.prototype.createCurrentEdgeStyle = function() { var style = 'edgeStyle=' + (this.currentEdgeStyle['edgeStyle'] || 'none') + ';'; if (this.currentEdgeStyle['shape'] != null) { style += 'shape=' + this.currentEdgeStyle['shape'] + ';'; } if (this.currentEdgeStyle['curved'] != null) { style += 'curved=' + this.currentEdgeStyle['curved'] + ';'; } if (this.currentEdgeStyle['rounded'] != null) { style += 'rounded=' + this.currentEdgeStyle['rounded'] + ';'; } if (this.currentEdgeStyle['comic'] != null) { style += 'comic=' + this.currentEdgeStyle['comic'] + ';'; } if (this.currentEdgeStyle['jumpStyle'] != null) { style += 'jumpStyle=' + this.currentEdgeStyle['jumpStyle'] + ';'; } if (this.currentEdgeStyle['jumpSize'] != null) { style += 'jumpSize=' + this.currentEdgeStyle['jumpSize'] + ';'; } // Overrides the global default to match the default edge style if (this.currentEdgeStyle['orthogonalLoop'] != null) { style += 'orthogonalLoop=' + this.currentEdgeStyle['orthogonalLoop'] + ';'; } else if (Graph.prototype.defaultEdgeStyle['orthogonalLoop'] != null) { style += 'orthogonalLoop=' + Graph.prototype.defaultEdgeStyle['orthogonalLoop'] + ';'; } // Overrides the global default to match the default edge style if (this.currentEdgeStyle['jettySize'] != null) { style += 'jettySize=' + this.currentEdgeStyle['jettySize'] + ';'; } else if (Graph.prototype.defaultEdgeStyle['jettySize'] != null) { style += 'jettySize=' + Graph.prototype.defaultEdgeStyle['jettySize'] + ';'; } // Special logic for custom property of elbowEdgeStyle if (this.currentEdgeStyle['edgeStyle'] == 'elbowEdgeStyle' && this.currentEdgeStyle['elbow'] != null) { style += 'elbow=' + this.currentEdgeStyle['elbow'] + ';'; } if (this.currentEdgeStyle['html'] != null) { style += 'html=' + this.currentEdgeStyle['html'] + ';'; } else { style += 'html=1;'; } return style; }; /** * Hook for subclassers. */ Graph.prototype.getPagePadding = function() { return new mxPoint(0, 0); }; /** * Loads the stylesheet for this graph. */ Graph.prototype.loadStylesheet = function() { var node = (this.themes != null) ? this.themes[this.defaultThemeName] : (!mxStyleRegistry.dynamicLoading) ? null : mxUtils.load(STYLE_PATH + '/default.xml').getDocumentElement(); if (node != null) { var dec = new mxCodec(node.ownerDocument); dec.decode(node, this.getStylesheet()); } }; /** * Creates lookup from object IDs to cell IDs. */ Graph.prototype.createCellLookup = function(cells, lookup) { lookup = (lookup != null) ? lookup : new Object(); for (var i = 0; i < cells.length; i++) { var cell = cells[i]; lookup[mxObjectIdentity.get(cell)] = cell.getId(); var childCount = this.model.getChildCount(cell); for (var j = 0; j < childCount; j++) { this.createCellLookup([this.model.getChildAt(cell, j)], lookup); } } return lookup; }; /** * Creates lookup from original to cloned cell IDs where mapping is * the mapping used in cloneCells and lookup is a mapping from * object IDs to cell IDs. */ Graph.prototype.createCellMapping = function(mapping, lookup, cellMapping) { cellMapping = (cellMapping != null) ? cellMapping : new Object(); for (var objectId in mapping) { var cellId = lookup[objectId]; if (cellMapping[cellId] == null) { // Uses empty string if clone ID was null which means // the cell was cloned but not inserted into the model. cellMapping[cellId] = mapping[objectId].getId() || ''; } } return cellMapping; }; /** * */ Graph.prototype.importGraphModel = function(node, dx, dy, crop) { dx = (dx != null) ? dx : 0; dy = (dy != null) ? dy : 0; var codec = new mxCodec(node.ownerDocument); var tempModel = new mxGraphModel(); codec.decode(node, tempModel); var cells = [] // Clones cells to remove invalid edges var cloneMap = new Object(); var cellMapping = new Object(); var layers = tempModel.getChildren(this.cloneCell(tempModel.root, this.isCloneInvalidEdges(), cloneMap)); if (layers != null) { // Creates lookup from object IDs to cell IDs var lookup = this.createCellLookup([tempModel.root]); // Uses copy as layers are removed from array inside loop layers = layers.slice(); this.model.beginUpdate(); try { // Merges into unlocked current layer if one layer is pasted if (layers.length == 1 && !this.isCellLocked(this.getDefaultParent())) { cells = this.moveCells(tempModel.getChildren(layers[0]), dx, dy, false, this.getDefaultParent()); // Imported default parent maps to local default parent cellMapping[tempModel.getChildAt(tempModel.root, 0).getId()] = this.getDefaultParent().getId(); } else { for (var i = 0; i < layers.length; i++) { var children = this.model.getChildren(this.moveCells( [layers[i]], dx, dy, false, this.model.getRoot())[0]); if (children != null) { cells = cells.concat(children); } } } if (cells != null) { // Adds mapping for all cloned entries from imported to local cell ID this.createCellMapping(cloneMap, lookup, cellMapping); this.updateCustomLinks(cellMapping, cells); if (crop) { if (this.isGridEnabled()) { dx = this.snap(dx); dy = this.snap(dy); } var bounds = this.getBoundingBoxFromGeometry(cells, true); if (bounds != null) { this.moveCells(cells, dx - bounds.x, dy - bounds.y); } } } } finally { this.model.endUpdate(); } } return cells; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ Graph.prototype.encodeCells = function(cells) { var cloneMap = new Object(); var clones = this.cloneCells(cells, null, cloneMap); // Creates a dictionary for fast lookups var dict = new mxDictionary(); for (var i = 0; i < cells.length; i++) { dict.put(cells[i], true); } // Checks for orphaned relative children and makes absolute for (var i = 0; i < clones.length; i++) { var state = this.view.getState(cells[i]); if (state != null) { var geo = this.getCellGeometry(clones[i]); if (geo != null && geo.relative && !this.model.isEdge(cells[i]) && !dict.get(this.model.getParent(cells[i]))) { geo.relative = false; geo.x = state.x / state.view.scale - state.view.translate.x; geo.y = state.y / state.view.scale - state.view.translate.y; } } } var codec = new mxCodec(); var model = new mxGraphModel(); var parent = model.getChildAt(model.getRoot(), 0); for (var i = 0; i < clones.length; i++) { model.add(parent, clones[i]); } this.updateCustomLinks(this.createCellMapping(cloneMap, this.createCellLookup(cells)), clones); return codec.encode(model); }; /** * Overrides cloning cells in moveCells. */ var graphMoveCells = Graph.prototype.moveCells; Graph.prototype.moveCells = function(cells, dx, dy, clone, target, evt, mapping) { mapping = (mapping != null) ? mapping : new Object(); var result = graphMoveCells.apply(this, arguments); if (clone) { this.updateCustomLinks(this.createCellMapping(mapping, this.createCellLookup(cells)), result); } return result; }; /** * Updates cells IDs for custom links in the given cells. */ Graph.prototype.updateCustomLinks = function(mapping, cells) { for (var i = 0; i < cells.length; i++) { if (cells[i] != null) { this.updateCustomLinksForCell(mapping, cells[i]); } } }; /** * Updates cell IDs in custom links on the given cell and its label. */ Graph.prototype.updateCustomLinksForCell = function(mapping, cell) { // Hook for subclassers }; /** * Overrides method to provide connection constraints for shapes. */ Graph.prototype.getAllConnectionConstraints = function(terminal, source) { if (terminal != null) { var constraints = mxUtils.getValue(terminal.style, 'points', null); if (constraints != null) { // Requires an array of arrays with x, y (0..1), an optional // [perimeter (0 or 1), dx, and dy] eg. points=[[0,0,1,-10,10],[0,1,0],[1,1]] var result = []; try { var c = JSON.parse(constraints); for (var i = 0; i < c.length; i++) { var tmp = c[i]; result.push(new mxConnectionConstraint(new mxPoint(tmp[0], tmp[1]), (tmp.length > 2) ? tmp[2] != '0' : true, null, (tmp.length > 3) ? tmp[3] : 0, (tmp.length > 4) ? tmp[4] : 0)); } } catch (e) { // ignore } return result; } else if (terminal.shape != null && terminal.shape.bounds != null) { var dir = terminal.shape.direction; var bounds = terminal.shape.bounds; var scale = terminal.shape.scale; var w = bounds.width / scale; var h = bounds.height / scale; if (dir == mxConstants.DIRECTION_NORTH || dir == mxConstants.DIRECTION_SOUTH) { var tmp = w; w = h; h = tmp; } constraints = terminal.shape.getConstraints(terminal.style, w, h); if (constraints != null) { return constraints; } else if (terminal.shape.stencil != null && terminal.shape.stencil.constraints != null) { return terminal.shape.stencil.constraints; } else if (terminal.shape.constraints != null) { return terminal.shape.constraints; } } } return null; }; /** * Inverts the elbow edge style without removing existing styles. */ Graph.prototype.flipEdge = function(edge) { if (edge != null) { var style = this.getCurrentCellStyle(edge); var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW, mxConstants.ELBOW_HORIZONTAL); var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ? mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL; this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]); } }; /** * Disables drill-down for non-swimlanes. */ Graph.prototype.isValidRoot = function(cell) { // Counts non-relative children var childCount = this.model.getChildCount(cell); var realChildCount = 0; for (var i = 0; i < childCount; i++) { var child = this.model.getChildAt(cell, i); if (this.model.isVertex(child)) { var geometry = this.getCellGeometry(child); if (geometry != null && !geometry.relative) { realChildCount++; } } } return realChildCount > 0 || this.isContainer(cell); }; /** * Disables drill-down for non-swimlanes. */ Graph.prototype.isValidDropTarget = function(cell) { var style = this.getCurrentCellStyle(cell); return (mxUtils.getValue(style, 'part', '0') != '1' || this.isContainer(cell)) && mxUtils.getValue(style, 'dropTarget', '1') != '0' && (mxGraph.prototype.isValidDropTarget.apply(this, arguments) || this.isContainer(cell)); }; /** * Overrides createGroupCell to set the group style for new groups to 'group'. */ Graph.prototype.createGroupCell = function() { var group = mxGraph.prototype.createGroupCell.apply(this, arguments); group.setStyle('group'); return group; }; /** * Disables extending parents with stack layouts on add */ Graph.prototype.isExtendParentsOnAdd = function(cell) { var result = mxGraph.prototype.isExtendParentsOnAdd.apply(this, arguments); if (result && cell != null && this.layoutManager != null) { var parent = this.model.getParent(cell); if (parent != null) { var layout = this.layoutManager.getLayout(parent); if (layout != null && layout.constructor == mxStackLayout) { result = false; } } } return result; }; /** * Overrides autosize to add a border. */ Graph.prototype.getPreferredSizeForCell = function(cell) { var result = mxGraph.prototype.getPreferredSizeForCell.apply(this, arguments); // Adds buffer if (result != null) { result.width += 10; result.height += 4; if (this.gridEnabled) { result.width = this.snap(result.width); result.height = this.snap(result.height); } } return result; } /** * Turns the given cells and returns the changed cells. */ Graph.prototype.turnShapes = function(cells, backwards) { var model = this.getModel(); var select = []; model.beginUpdate(); try { for (var i = 0; i < cells.length; i++) { var cell = cells[i]; if (model.isEdge(cell)) { var src = model.getTerminal(cell, true); var trg = model.getTerminal(cell, false); model.setTerminal(cell, trg, true); model.setTerminal(cell, src, false); var geo = model.getGeometry(cell); if (geo != null) { geo = geo.clone(); if (geo.points != null) { geo.points.reverse(); } var sp = geo.getTerminalPoint(true); var tp = geo.getTerminalPoint(false) geo.setTerminalPoint(sp, false); geo.setTerminalPoint(tp, true); model.setGeometry(cell, geo); // Inverts constraints var edgeState = this.view.getState(cell); var sourceState = this.view.getState(src); var targetState = this.view.getState(trg); if (edgeState != null) { var sc = (sourceState != null) ? this.getConnectionConstraint(edgeState, sourceState, true) : null; var tc = (targetState != null) ? this.getConnectionConstraint(edgeState, targetState, false) : null; this.setConnectionConstraint(cell, src, true, tc); this.setConnectionConstraint(cell, trg, false, sc); } select.push(cell); } } else if (model.isVertex(cell)) { var geo = this.getCellGeometry(cell); if (geo != null) { // Rotates the size and position in the geometry geo = geo.clone(); geo.x += geo.width / 2 - geo.height / 2; geo.y += geo.height / 2 - geo.width / 2; var tmp = geo.width; geo.width = geo.height; geo.height = tmp; model.setGeometry(cell, geo); // Reads the current direction and advances by 90 degrees var state = this.view.getState(cell); if (state != null) { var dirs = [mxConstants.DIRECTION_EAST, mxConstants.DIRECTION_SOUTH, mxConstants.DIRECTION_WEST, mxConstants.DIRECTION_NORTH]; var dir = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST); this.setCellStyles(mxConstants.STYLE_DIRECTION, dirs[mxUtils.mod(mxUtils.indexOf(dirs, dir) + ((backwards) ? -1 : 1), dirs.length)], [cell]); } select.push(cell); } } } } finally { model.endUpdate(); } return select; }; /** * Returns true if the given stencil contains any placeholder text. */ Graph.prototype.stencilHasPlaceholders = function(stencil) { if (stencil != null && stencil.fgNode != null) { var node = stencil.fgNode.firstChild; while (node != null) { if (node.nodeName == 'text' && node.getAttribute('placeholders') == '1') { return true; } node = node.nextSibling; } } return false; }; /** * Updates the child cells with placeholders if metadata of a cell has changed. */ Graph.prototype.processChange = function(change) { mxGraph.prototype.processChange.apply(this, arguments); if (change instanceof mxValueChange && change.cell != null && change.cell.value != null && typeof(change.cell.value) == 'object') { this.invalidateDescendantsWithPlaceholders(change.cell); } }; /** * Replaces the given element with a span. */ Graph.prototype.invalidateDescendantsWithPlaceholders = function(cell) { // Invalidates all descendants with placeholders var desc = this.model.getDescendants(cell); // LATER: Check if only label or tooltip have changed if (desc.length > 0) { for (var i = 0; i < desc.length; i++) { var state = this.view.getState(desc[i]); if (state != null && state.shape != null && state.shape.stencil != null && this.stencilHasPlaceholders(state.shape.stencil)) { this.removeStateForCell(desc[i]); } else if (this.isReplacePlaceholders(desc[i])) { this.view.invalidate(desc[i], false, false); } } } }; /** * Replaces the given element with a span. */ Graph.prototype.replaceElement = function(elt, tagName) { var span = elt.ownerDocument.createElement((tagName != null) ? tagName : 'span'); var attributes = Array.prototype.slice.call(elt.attributes); while (attr = attributes.pop()) { span.setAttribute(attr.nodeName, attr.nodeValue); } span.innerHTML = elt.innerHTML; elt.parentNode.replaceChild(span, elt); }; /** * */ Graph.prototype.processElements = function(elt, fn) { if (elt != null) { var elts = elt.getElementsByTagName('*'); for (var i = 0; i < elts.length; i++) { fn(elts[i]); } } }; /** * Handles label changes for XML user objects. */ Graph.prototype.updateLabelElements = function(cells, fn, tagName) { cells = (cells != null) ? cells : this.getSelectionCells(); var div = document.createElement('div'); for (var i = 0; i < cells.length; i++) { // Changes font tags inside HTML labels if (this.isHtmlLabel(cells[i])) { var label = this.convertValueToString(cells[i]); if (label != null && label.length > 0) { div.innerHTML = label; var elts = div.getElementsByTagName((tagName != null) ? tagName : '*'); for (var j = 0; j < elts.length; j++) { fn(elts[j]); } if (div.innerHTML != label) { this.cellLabelChanged(cells[i], div.innerHTML); } } } } }; /** * Handles label changes for XML user objects. */ Graph.prototype.cellLabelChanged = function(cell, value, autoSize) { // Removes all illegal control characters in user input value = Graph.zapGremlins(value); this.model.beginUpdate(); try { if (cell.value != null && typeof cell.value == 'object') { if (this.isReplacePlaceholders(cell) && cell.getAttribute('placeholder') != null) { // LATER: Handle delete, name change var name = cell.getAttribute('placeholder'); var current = cell; while (current != null) { if (current == this.model.getRoot() || (current.value != null && typeof(current.value) == 'object' && current.hasAttribute(name))) { this.setAttributeForCell(current, name, value); break; } current = this.model.getParent(current); } } var tmp = cell.value.cloneNode(true); tmp.setAttribute('label', value); value = tmp; } mxGraph.prototype.cellLabelChanged.apply(this, arguments); } finally { this.model.endUpdate(); } }; /** * Removes transparent empty groups if all children are removed. */ Graph.prototype.cellsRemoved = function(cells) { if (cells != null) { var dict = new mxDictionary(); for (var i = 0; i < cells.length; i++) { dict.put(cells[i], true); } // LATER: Recurse up the cell hierarchy var parents = []; for (var i = 0; i < cells.length; i++) { var parent = this.model.getParent(cells[i]); if (parent != null && !dict.get(parent)) { dict.put(parent, true); parents.push(parent); } } for (var i = 0; i < parents.length; i++) { var state = this.view.getState(parents[i]); if (state != null && (this.model.isEdge(state.cell) || this.model.isVertex(state.cell)) && this.isCellDeletable(state.cell) && this.isTransparentState(state)) { var allChildren = true; for (var j = 0; j < this.model.getChildCount(state.cell) && allChildren; j++) { if (!dict.get(this.model.getChildAt(state.cell, j))) { allChildren = false; } } if (allChildren) { cells.push(state.cell); } } } } mxGraph.prototype.cellsRemoved.apply(this, arguments); }; /** * Overrides ungroup to check if group should be removed. */ Graph.prototype.removeCellsAfterUngroup = function(cells) { var cellsToRemove = []; for (var i = 0; i < cells.length; i++) { if (this.isCellDeletable(cells[i]) && this.isTransparentState( this.view.getState(cells[i]))) { cellsToRemove.push(cells[i]); } } cells = cellsToRemove; mxGraph.prototype.removeCellsAfterUngroup.apply(this, arguments); }; /** * Sets the link for the given cell. */ Graph.prototype.setLinkForCell = function(cell, link) { this.setAttributeForCell(cell, 'link', link); }; /** * Sets the link for the given cell. */ Graph.prototype.setTooltipForCell = function(cell, link) { this.setAttributeForCell(cell, 'tooltip', link); }; /** * Returns the cells in the model (or given array) that have all of the * given tags in their tags property. */ Graph.prototype.getAttributeForCell = function(cell, attributeName, defaultValue) { var value = (cell.value != null && typeof cell.value === 'object') ? cell.value.getAttribute(attributeName) : null; return (value != null) ? value : defaultValue; }; /** * Sets the link for the given cell. */ Graph.prototype.setAttributeForCell = function(cell, attributeName, attributeValue) { var value = null; if (cell.value != null && typeof(cell.value) == 'object') { value = cell.value.cloneNode(true); } else { var doc = mxUtils.createXmlDocument(); value = doc.createElement('UserObject'); value.setAttribute('label', cell.value || ''); } if (attributeValue != null) { value.setAttribute(attributeName, attributeValue); } else { value.removeAttribute(attributeName); } this.model.setValue(cell, value); }; /** * Overridden to stop moving edge labels between cells. */ Graph.prototype.getDropTarget = function(cells, evt, cell, clone) { var model = this.getModel(); // Disables drop into group if alt is pressed if (mxEvent.isAltDown(evt)) { return null; } // Disables dragging edge labels out of edges for (var i = 0; i < cells.length; i++) { if (this.model.isEdge(this.model.getParent(cells[i]))) { return null; } } return mxGraph.prototype.getDropTarget.apply(this, arguments); }; /** * Overrides double click handling to avoid accidental inserts of new labels in dblClick below. */ Graph.prototype.click = function(me) { mxGraph.prototype.click.call(this, me); // Stores state and source for checking in dblClick this.firstClickState = me.getState(); this.firstClickSource = me.getSource(); }; /** * Overrides double click handling to add the tolerance and inserting text. */ Graph.prototype.dblClick = function(evt, cell) { if (this.isEnabled()) { var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); // Automatically adds new child cells to edges on double click if (evt != null && !this.model.isVertex(cell)) { var state = (this.model.isEdge(cell)) ? this.view.getState(cell) : null; var src = mxEvent.getSource(evt); if ((this.firstClickState == state && this.firstClickSource == src) && (state == null || (state.text == null || state.text.node == null || state.text.boundingBox == null || (!mxUtils.contains(state.text.boundingBox, pt.x, pt.y) && !mxUtils.isAncestorNode(state.text.node, mxEvent.getSource(evt))))) && ((state == null && !this.isCellLocked(this.getDefaultParent())) || (state != null && !this.isCellLocked(state.cell))) && (state != null || (mxClient.IS_VML && src == this.view.getCanvas()) || (mxClient.IS_SVG && src == this.view.getCanvas().ownerSVGElement))) { if (state == null) { state = this.view.getState(this.getCellAt(pt.x, pt.y)); } cell = this.addText(pt.x, pt.y, state); } } mxGraph.prototype.dblClick.call(this, evt, cell); } }; /** * Returns a point that specifies the location for inserting cells. */ Graph.prototype.getInsertPoint = function() { var gs = this.getGridSize(); var dx = this.container.scrollLeft / this.view.scale - this.view.translate.x; var dy = this.container.scrollTop / this.view.scale - this.view.translate.y; if (this.pageVisible) { var layout = this.getPageLayout(); var page = this.getPageSize(); dx = Math.max(dx, layout.x * page.width); dy = Math.max(dy, layout.y * page.height); } return new mxPoint(this.snap(dx + gs), this.snap(dy + gs)); }; /** * */ Graph.prototype.getFreeInsertPoint = function() { var view = this.view; var bds = this.getGraphBounds(); var pt = this.getInsertPoint(); // Places at same x-coord and 2 grid sizes below existing graph var x = this.snap(Math.round(Math.max(pt.x, bds.x / view.scale - view.translate.x + ((bds.width == 0) ? 2 * this.gridSize : 0)))); var y = this.snap(Math.round(Math.max(pt.y, (bds.y + bds.height) / view.scale - view.translate.y + 2 * this.gridSize))); return new mxPoint(x, y); }; /** * */ Graph.prototype.getCenterInsertPoint = function(bbox) { bbox = (bbox != null) ? bbox : new mxRectangle(); if (mxUtils.hasScrollbars(this.container)) { return new mxPoint( this.snap((this.container.scrollLeft + this.container.clientWidth / 2) / this.view.scale - this.view.translate.x - bbox.width / 2), this.snap((this.container.scrollTop + this.container.clientHeight / 2) / this.view.scale - this.view.translate.y - bbox.height / 2)); } else { return new mxPoint( this.snap(this.container.clientWidth / 2 / this.view.scale - this.view.translate.x - bbox.width / 2), this.snap(this.container.clientHeight / 2 / this.view.scale - this.view.translate.y - bbox.height / 2)); } }; /** * Hook for subclassers to return true if the current insert point was defined * using a mouse hover event. */ Graph.prototype.isMouseInsertPoint = function() { return false; }; /** * Adds a new label at the given position and returns the new cell. State is * an optional edge state to be used as the parent for the label. Vertices * are not allowed currently as states. */ Graph.prototype.addText = function(x, y, state) { // Creates a new edge label with a predefined text var label = new mxCell(); label.value = 'Text'; label.style = 'text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];' label.geometry = new mxGeometry(0, 0, 0, 0); label.vertex = true; if (state != null && this.model.isEdge(state.cell)) { label.style += 'labelBackgroundColor=#ffffff;' label.geometry.relative = true; label.connectable = false; // Resets the relative location stored inside the geometry var pt2 = this.view.getRelativePoint(state, x, y); label.geometry.x = Math.round(pt2.x * 10000) / 10000; label.geometry.y = Math.round(pt2.y); // Resets the offset inside the geometry to find the offset from the resulting point label.geometry.offset = new mxPoint(0, 0); pt2 = this.view.getPoint(state, label.geometry); var scale = this.view.scale; label.geometry.offset = new mxPoint(Math.round((x - pt2.x) / scale), Math.round((y - pt2.y) / scale)); } else { var tr = this.view.translate; label.geometry.width = 40; label.geometry.height = 20; label.geometry.x = Math.round(x / this.view.scale) - tr.x - ((state != null) ? state.origin.x : 0); label.geometry.y = Math.round(y / this.view.scale) - tr.y - ((state != null) ? state.origin.y : 0); label.style += 'autosize=1;' } this.getModel().beginUpdate(); try { this.addCells([label], (state != null) ? state.cell : null); this.fireEvent(new mxEventObject('textInserted', 'cells', [label])); // Updates size of text after possible change of style via event this.autoSizeCell(label); } finally { this.getModel().endUpdate(); } return label; }; /** * Adds a handler for clicking on shapes with links. This replaces all links in labels. */ Graph.prototype.addClickHandler = function(highlight, beforeClick, onClick) { // Replaces links in labels for consistent right-clicks var checkLinks = mxUtils.bind(this, function() { var links = this.container.getElementsByTagName('a'); if (links != null) { for (var i = 0; i < links.length; i++) { var href = this.getAbsoluteUrl(links[i].getAttribute('href')); if (href != null) { links[i].setAttribute('rel', this.linkRelation); links[i].setAttribute('href', href); if (beforeClick != null) { mxEvent.addGestureListeners(links[i], null, null, beforeClick); } } } } }); this.model.addListener(mxEvent.CHANGE, checkLinks); checkLinks(); var cursor = this.container.style.cursor; var tol = this.getTolerance(); var graph = this; var mouseListener = { currentState: null, currentLink: null, highlight: (highlight != null && highlight != '' && highlight != mxConstants.NONE) ? new mxCellHighlight(graph, highlight, 4) : null, startX: 0, startY: 0, scrollLeft: 0, scrollTop: 0, updateCurrentState: function(me) { var tmp = me.sourceState; // Gets topmost intersecting cell with link if (tmp == null || graph.getLinkForCell(tmp.cell) == null) { var cell = graph.getCellAt(me.getGraphX(), me.getGraphY(), null, null, null, function(state, x, y) { return graph.getLinkForCell(state.cell) == null; }); tmp = graph.view.getState(cell); } if (tmp != this.currentState) { if (this.currentState != null) { this.clear(); } this.currentState = tmp; if (this.currentState != null) { this.activate(this.currentState); } } }, mouseDown: function(sender, me) { this.startX = me.getGraphX(); this.startY = me.getGraphY(); this.scrollLeft = graph.container.scrollLeft; this.scrollTop = graph.container.scrollTop; if (this.currentLink == null && graph.container.style.overflow == 'auto') { graph.container.style.cursor = 'move'; } this.updateCurrentState(me); }, mouseMove: function(sender, me) { if (graph.isMouseDown) { if (this.currentLink != null) { var dx = Math.abs(this.startX - me.getGraphX()); var dy = Math.abs(this.startY - me.getGraphY()); if (dx > tol || dy > tol) { this.clear(); } } } else { // Checks for parent link var linkNode = me.getSource(); while (linkNode != null && linkNode.nodeName.toLowerCase() != 'a') { linkNode = linkNode.parentNode; } if (linkNode != null) { this.clear(); } else { if (graph.tooltipHandler != null && this.currentLink != null && this.currentState != null) { graph.tooltipHandler.reset(me, true, this.currentState); } if (this.currentState != null && (me.getState() == this.currentState || me.sourceState == null) && graph.intersects(this.currentState, me.getGraphX(), me.getGraphY())) { return; } this.updateCurrentState(me); } } }, mouseUp: function(sender, me) { var source = me.getSource(); var evt = me.getEvent(); // Checks for parent link var linkNode = source; while (linkNode != null && linkNode.nodeName.toLowerCase() != 'a') { linkNode = linkNode.parentNode; } // Ignores clicks on links and collapse/expand icon if (linkNode == null && (((Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol && Math.abs(this.scrollTop - graph.container.scrollTop) < tol) && (me.sourceState == null || !me.isSource(me.sourceState.control))) && (((mxEvent.isLeftMouseButton(evt) || mxEvent.isMiddleMouseButton(evt)) && !mxEvent.isPopupTrigger(evt)) || mxEvent.isTouchEvent(evt)))) { if (this.currentLink != null) { var blank = graph.isBlankLink(this.currentLink); if ((this.currentLink.substring(0, 5) === 'data:' || !blank) && beforeClick != null) { beforeClick(evt, this.currentLink); } if (!mxEvent.isConsumed(evt)) { var target = (mxEvent.isMiddleMouseButton(evt)) ? '_blank' : ((blank) ? graph.linkTarget : '_top'); graph.openLink(this.currentLink, target); me.consume(); } } else if (onClick != null && !me.isConsumed() && (Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol && Math.abs(this.scrollTop - graph.container.scrollTop) < tol) && (Math.abs(this.startX - me.getGraphX()) < tol && Math.abs(this.startY - me.getGraphY()) < tol)) { onClick(me.getEvent()); } } this.clear(); }, activate: function(state) { this.currentLink = graph.getAbsoluteUrl(graph.getLinkForCell(state.cell)); if (this.currentLink != null) { graph.container.style.cursor = 'pointer'; if (this.highlight != null) { this.highlight.highlight(state); } } }, clear: function() { if (graph.container != null) { graph.container.style.cursor = cursor; } this.currentState = null; this.currentLink = null; if (this.highlight != null) { this.highlight.hide(); } if (graph.tooltipHandler != null) { graph.tooltipHandler.hide(); } } }; // Ignores built-in click handling graph.click = function(me) {}; graph.addMouseListener(mouseListener); mxEvent.addListener(document, 'mouseleave', function(evt) { mouseListener.clear(); }); }; /** * Duplicates the given cells and returns the duplicates. */ Graph.prototype.duplicateCells = function(cells, append) { cells = (cells != null) ? cells : this.getSelectionCells(); append = (append != null) ? append : true; cells = this.model.getTopmostCells(cells); var model = this.getModel(); var s = this.gridSize; var select = []; model.beginUpdate(); try { var clones = this.cloneCells(cells, false, null, true); for (var i = 0; i < cells.length; i++) { var parent = model.getParent(cells[i]); var child = this.moveCells([clones[i]], s, s, false)[0]; select.push(child); if (append) { model.add(parent, clones[i]); } else { // Maintains child index by inserting after clone in parent var index = parent.getIndex(cells[i]); model.add(parent, clones[i], index + 1); } } } finally { model.endUpdate(); } return select; }; /** * Inserts the given image at the cursor in a content editable text box using * the insertimage command on the document instance. */ Graph.prototype.insertImage = function(newValue, w, h) { // To find the new image, we create a list of all existing links first if (newValue != null && this.cellEditor.textarea != null) { var tmp = this.cellEditor.textarea.getElementsByTagName('img'); var oldImages = []; for (var i = 0; i < tmp.length; i++) { oldImages.push(tmp[i]); } // LATER: Fix inserting link/image in IE8/quirks after focus lost document.execCommand('insertimage', false, newValue); // Sets size of new image var newImages = this.cellEditor.textarea.getElementsByTagName('img'); if (newImages.length == oldImages.length + 1) { // Inverse order in favor of appended images for (var i = newImages.length - 1; i >= 0; i--) { if (i == 0 || newImages[i] != oldImages[i - 1]) { // Workaround for lost styles during undo and redo is using attributes newImages[i].setAttribute('width', w); newImages[i].setAttribute('height', h); break; } } } } }; /** * Inserts the given image at the cursor in a content editable text box using * the insertimage command on the document instance. */ Graph.prototype.insertLink = function(value) { if (this.cellEditor.textarea != null) { if (value.length == 0) { document.execCommand('unlink', false); } else if (mxClient.IS_FF) { // Workaround for Firefox that adds a new link and removes // the href from the inner link if its parent is a span is // to remove all inner links inside the new outer link var tmp = this.cellEditor.textarea.getElementsByTagName('a'); var oldLinks = []; for (var i = 0; i < tmp.length; i++) { oldLinks.push(tmp[i]); } document.execCommand('createlink', false, mxUtils.trim(value)); // Finds the new link element var newLinks = this.cellEditor.textarea.getElementsByTagName('a'); if (newLinks.length == oldLinks.length + 1) { // Inverse order in favor of appended links for (var i = newLinks.length - 1; i >= 0; i--) { if (newLinks[i] != oldLinks[i - 1]) { // Removes all inner links from the new link and // moves the children to the inner link parent var tmp = newLinks[i].getElementsByTagName('a'); while (tmp.length > 0) { var parent = tmp[0].parentNode; while (tmp[0].firstChild != null) { parent.insertBefore(tmp[0].firstChild, tmp[0]); } parent.removeChild(tmp[0]); } break; } } } } else { // LATER: Fix inserting link/image in IE8/quirks after focus lost document.execCommand('createlink', false, mxUtils.trim(value)); } } }; /** * * @param cell * @returns {Boolean} */ Graph.prototype.isCellResizable = function(cell) { var result = mxGraph.prototype.isCellResizable.apply(this, arguments); var style = this.getCurrentCellStyle(cell); return result || (mxUtils.getValue(style, mxConstants.STYLE_RESIZABLE, '1') != '0' && style[mxConstants.STYLE_WHITE_SPACE] == 'wrap'); }; /** * Function: distributeCells * * Distribuets the centers of the given cells equally along the available * horizontal or vertical space. * * Parameters: * * horizontal - Boolean that specifies the direction of the distribution. * cells - Optional array of to be distributed. Edges are ignored. */ Graph.prototype.distributeCells = function(horizontal, cells) { if (cells == null) { cells = this.getSelectionCells(); } if (cells != null && cells.length > 1) { var vertices = []; var max = null; var min = null; for (var i = 0; i < cells.length; i++) { if (this.getModel().isVertex(cells[i])) { var state = this.view.getState(cells[i]); if (state != null) { var tmp = (horizontal) ? state.getCenterX() : state.getCenterY(); max = (max != null) ? Math.max(max, tmp) : tmp; min = (min != null) ? Math.min(min, tmp) : tmp; vertices.push(state); } } } if (vertices.length > 2) { vertices.sort(function(a, b) { return (horizontal) ? a.x - b.x : a.y - b.y; }); var t = this.view.translate; var s = this.view.scale; min = min / s - ((horizontal) ? t.x : t.y); max = max / s - ((horizontal) ? t.x : t.y); this.getModel().beginUpdate(); try { var dt = (max - min) / (vertices.length - 1); var t0 = min; for (var i = 1; i < vertices.length - 1; i++) { var pstate = this.view.getState(this.model.getParent(vertices[i].cell)); var geo = this.getCellGeometry(vertices[i].cell); t0 += dt; if (geo != null && pstate != null) { geo = geo.clone(); if (horizontal) { geo.x = Math.round(t0 - geo.width / 2) - pstate.origin.x; } else { geo.y = Math.round(t0 - geo.height / 2) - pstate.origin.y; } this.getModel().setGeometry(vertices[i].cell, geo); } } } finally { this.getModel().endUpdate(); } } } return cells; }; /** * Adds meta-drag an Mac. * @param evt * @returns */ Graph.prototype.isCloneEvent = function(evt) { return (mxClient.IS_MAC && mxEvent.isMetaDown(evt)) || mxEvent.isControlDown(evt); }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ Graph.prototype.createSvgImageExport = function() { var exp = new mxImageExport(); // Adds hyperlinks (experimental) exp.getLinkForCellState = mxUtils.bind(this, function(state, canvas) { return this.getLinkForCell(state.cell); }); return exp; }; /** * Translates this point by the given vector. * * @param {number} dx X-coordinate of the translation. * @param {number} dy Y-coordinate of the translation. */ Graph.prototype.getSvg = function(background, scale, border, nocrop, crisp, ignoreSelection, showText, imgExport, linkTarget, hasShadow) { //Disable Css Transforms if it is used var origUseCssTrans = this.useCssTransforms; if (origUseCssTrans) { this.useCssTransforms = false; this.view.revalidate(); this.sizeDidChange(); } try { scale = (scale != null) ? scale : 1; border = (border != null) ? border : 0; crisp = (crisp != null) ? crisp : true; ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; showText = (showText != null) ? showText : true; var bounds = (ignoreSelection || nocrop) ? this.getGraphBounds() : this.getBoundingBox( this.getSelectionCells()); if (bounds == null) { throw Error(mxResources.get('drawingEmpty')); } var vs = this.view.scale; // Prepares SVG document that holds the output 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); } var s = scale / vs; var w = Math.max(1, Math.ceil(bounds.width * s) + 2 * border) + ((hasShadow) ? 5 : 0); var h = Math.max(1, Math.ceil(bounds.height * s) + 2 * border) + ((hasShadow) ? 5 : 0); root.setAttribute('version', '1.1'); root.setAttribute('width', w + 'px'); root.setAttribute('height', h + 'px'); root.setAttribute('viewBox', ((crisp) ? '-0.5 -0.5' : '0 0') + ' ' + w + ' ' + h); svgDoc.appendChild(root); // Renders graph. Offset will be multiplied with state's scale when painting state. // TextOffset only seems to affect FF output but used everywhere for consistency. var group = (svgDoc.createElementNS != null) ? svgDoc.createElementNS(mxConstants.NS_SVG, 'g') : svgDoc.createElement('g'); root.appendChild(group); var svgCanvas = this.createSvgCanvas(group); svgCanvas.foOffset = (crisp) ? -0.5 : 0; svgCanvas.textOffset = (crisp) ? -0.5 : 0; svgCanvas.imageOffset = (crisp) ? -0.5 : 0; svgCanvas.translate(Math.floor((border / scale - bounds.x) / vs), Math.floor((border / scale - bounds.y) / vs)); // Convert HTML entities var htmlConverter = document.createElement('div'); // Adds simple text fallback for viewers with no support for foreignObjects var getAlternateText = svgCanvas.getAlternateText; svgCanvas.getAlternateText = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation) { // Assumes a max character width of 0.5em if (str != null && this.state.fontSize > 0) { try { if (mxUtils.isNode(str)) { str = str.innerText; } else { htmlConverter.innerHTML = str; str = mxUtils.extractTextWithWhitespace(htmlConverter.childNodes); } // Workaround for substring breaking double byte UTF var exp = Math.ceil(2 * w / this.state.fontSize); var result = []; var length = 0; var index = 0; while ((exp == 0 || length < exp) && index < str.length) { var char = str.charCodeAt(index); if (char == 10 || char == 13) { if (length > 0) { break; } } else { result.push(str.charAt(index)); if (char < 255) { length++; } } index++; } // Uses result and adds ellipsis if more than 1 char remains if (result.length < str.length && str.length - result.length > 1) { str = mxUtils.trim(result.join('')) + '...'; } return str; } catch (e) { return getAlternateText.apply(this, arguments); } } else { return getAlternateText.apply(this, arguments); } }; // Paints background image var bgImg = this.backgroundImage; if (bgImg != null) { var s2 = vs / scale; var tr = this.view.translate; var tmp = new mxRectangle(tr.x * s2, tr.y * s2, bgImg.width * s2, bgImg.height * s2); // Checks if visible if (mxUtils.intersects(bounds, tmp)) { svgCanvas.image(tr.x, tr.y, bgImg.width, bgImg.height, bgImg.src, true); } } svgCanvas.scale(s); svgCanvas.textEnabled = showText; imgExport = (imgExport != null) ? imgExport : this.createSvgImageExport(); var imgExportDrawCellState = imgExport.drawCellState; // Ignores custom links var imgExportGetLinkForCellState = imgExport.getLinkForCellState; imgExport.getLinkForCellState = function(state, canvas) { var result = imgExportGetLinkForCellState.apply(this, arguments); return (result != null && !state.view.graph.isCustomLink(result)) ? result : null; }; // Implements ignoreSelection flag imgExport.drawCellState = function(state, canvas) { var graph = state.view.graph; var selected = graph.isCellSelected(state.cell); var parent = graph.model.getParent(state.cell); // Checks if parent cell is selected while (!ignoreSelection && !selected && parent != null) { selected = graph.isCellSelected(parent); parent = graph.model.getParent(parent); } if (ignoreSelection || selected) { imgExportDrawCellState.apply(this, arguments); } }; imgExport.drawState(this.getView().getState(this.model.root), svgCanvas); this.updateSvgLinks(root, linkTarget, true); this.addForeignObjectWarning(svgCanvas, root); return root; } finally { if (origUseCssTrans) { this.useCssTransforms = true; this.view.revalidate(); this.sizeDidChange(); } } }; /** * Adds warning for truncated labels in older viewers. */ Graph.prototype.addForeignObjectWarning = function(canvas, root) { if (root.getElementsByTagName('foreignObject').length > 0) { var sw = canvas.createElement('switch'); var g1 = canvas.createElement('g'); g1.setAttribute('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility'); var a = canvas.createElement('a'); a.setAttribute('transform', 'translate(0,-5)'); // Workaround for implicit namespace handling in HTML5 export, IE adds NS1 namespace so use code below // in all IE versions except quirks mode. KNOWN: Adds xlink namespace to each image tag in output. if (a.setAttributeNS == null || (root.ownerDocument != document && document.documentMode == null)) { a.setAttribute('xlink:href', Graph.foreignObjectWarningLink); a.setAttribute('target', '_blank'); } else { a.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', Graph.foreignObjectWarningLink); a.setAttributeNS(mxConstants.NS_XLINK, 'target', '_blank'); } var text = canvas.createElement('text'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('font-size', '10px'); text.setAttribute('x', '50%'); text.setAttribute('y', '100%'); mxUtils.write(text, Graph.foreignObjectWarningText); sw.appendChild(g1); a.appendChild(text); sw.appendChild(a); root.appendChild(sw); } }; /** * Hook for creating the canvas used in getSvg. */ Graph.prototype.updateSvgLinks = function(node, target, removeCustom) { var links = node.getElementsByTagName('a'); for (var i = 0; i < links.length; i++) { var href = links[i].getAttribute('href'); if (href == null) { href = links[i].getAttribute('xlink:href'); } if (href != null) { if (target != null && /^https?:\/\//.test(href)) { links[i].setAttribute('target', target); } else if (removeCustom && this.isCustomLink(href)) { links[i].setAttribute('href', 'javascript:void(0);'); } } } }; /** * Hook for creating the canvas used in getSvg. */ Graph.prototype.createSvgCanvas = function(node) { var canvas = new mxSvgCanvas2D(node); canvas.pointerEvents = true; return canvas; }; /** * Returns the first ancestor of the current selection with the given name. */ Graph.prototype.getSelectedElement = function() { var node = null; if (window.getSelection) { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { var range = sel.getRangeAt(0); node = range.commonAncestorContainer; } } else if (document.selection) { node = document.selection.createRange().parentElement(); } return node; }; /** * Returns the first ancestor of the current selection with the given name. */ Graph.prototype.getParentByName = function(node, name, stopAt) { while (node != null) { if (node.nodeName == name) { return node; } if (node == stopAt) { return null; } node = node.parentNode; } return node; }; /** * Returns the first ancestor of the current selection with the given name. */ Graph.prototype.getParentByNames = function(node, names, stopAt) { while (node != null) { if (mxUtils.indexOf(names, node.nodeName) >= 0) { return node; } if (node == stopAt) { return null; } node = node.parentNode; } return node; }; /** * Selects the given node. */ Graph.prototype.selectNode = function(node) { var sel = null; // IE9 and non-IE if (window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { var range = document.createRange(); range.selectNode(node); sel.removeAllRanges(); sel.addRange(range); } } // IE < 9 else if ((sel = document.selection) && sel.type != 'Control') { var originalRange = sel.createRange(); originalRange.collapse(true); var range = sel.createRange(); range.setEndPoint('StartToStart', originalRange); range.select(); } }; /** * */ Graph.prototype.insertTableColumn = function(cell, before) { var model = this.getModel(); model.beginUpdate(); try { var table = cell; var index = 0; if (this.isTableCell(cell)) { var row = model.getParent(cell); table = model.getParent(row); index = row.getIndex(cell); } else { if (this.isTableRow(cell)) { table = model.getParent(cell); } if (!before) { index = model.getChildCount(model.getChildAt(table, 0)) - 1; } } for (var i = 0; i < model.getChildCount(table); i++) { var row = model.getChildAt(table, i); var child = model.getChildAt(row, index); var clone = model.cloneCell(child); var geo = this.getCellGeometry(clone); clone.value = null; if (geo != null) { geo.width = Graph.minTableColumnWidth; var rowGeo = this.getCellGeometry(row); if (rowGeo != null) { geo.height = rowGeo.height; } } model.add(row, clone, index + ((before) ? 0 : 1)); } var tableGeo = this.getCellGeometry(table); if (tableGeo != null) { tableGeo = tableGeo.clone(); tableGeo.width += Graph.minTableColumnWidth; model.setGeometry(table, tableGeo); } } finally { model.endUpdate(); } }; /** * */ Graph.prototype.insertTableRow = function(cell, before) { var model = this.getModel(); model.beginUpdate(); try { var table = cell; var index = 0; if (this.isTableCell(cell)) { var row = model.getParent(cell); table = model.getParent(row); index = table.getIndex(row); } else if (this.isTableRow(cell)) { table = model.getParent(cell); index = table.getIndex(cell); } else if (!before) { index = model.getChildCount(table) - 1; } var row = model.cloneCell(model.getChildAt(table, index)); row.value = null; var rowGeo = this.getCellGeometry(row); if (rowGeo != null) { rowGeo.height = Graph.minTableRowHeight; for (var i = 0; i < model.getChildCount(row); i++) { var cell = model.getChildAt(row, i); cell.value = null; var geo = this.getCellGeometry(cell); if (geo != null) { geo.height = rowGeo.height; } } model.add(table, row, index + ((before) ? 0 : 1)); var tableGeo = this.getCellGeometry(table); if (tableGeo != null) { tableGeo = tableGeo.clone(); tableGeo.height += rowGeo.height; model.setGeometry(table, tableGeo); } } } finally { model.endUpdate(); } }; /** * */ Graph.prototype.deleteTableColumn = function(cell) { var model = this.getModel(); model.beginUpdate(); try { var table = cell; var index = 0; if (this.isTableCell(cell)) { var row = model.getParent(cell); table = model.getParent(row); index = row.getIndex(cell); } else if (this.isTableRow(cell)) { table = model.getParent(cell); index = model.getChildCount(cell) - 1; } else if (this.isTable(cell)) { index = model.getChildCount(model.getChildAt(cell, 0)) - 1; } var width = 0; for (var i = 0; i < model.getChildCount(table); i++) { var row = model.getChildAt(table, i); var child = model.getChildAt(row, index); model.remove(child); var geo = this.getCellGeometry(child); if (geo != null) { width = Math.max(width, geo.width); } } var tableGeo = this.getCellGeometry(table); if (tableGeo != null) { tableGeo = tableGeo.clone(); tableGeo.width -= width; model.setGeometry(table, tableGeo); } } finally { model.endUpdate(); } }; /** * */ Graph.prototype.deleteTableRow = function(cell) { var model = this.getModel(); model.beginUpdate(); try { var row = cell; if (this.isTableCell(cell)) { row = model.getParent(cell); } else if (this.isTable(cell)) { row = model.getChildAt(cell, model.getChildCount(cell) - 1); } var table = model.getParent(row); model.remove(row); var height = 0; var geo = this.getCellGeometry(row); if (geo != null) { height = geo.height; } var tableGeo = this.getCellGeometry(table); if (tableGeo != null) { tableGeo = tableGeo.clone(); tableGeo.height -= height; model.setGeometry(table, tableGeo); } } finally { model.endUpdate(); } }; /** * Inserts a new row into the given table. */ Graph.prototype.insertRow = function(table, index) { var bd = table.tBodies[0]; var cells = bd.rows[0].cells; var cols = 0; // Counts columns including colspans for (var i = 0; i < cells.length; i++) { var colspan = cells[i].getAttribute('colspan'); cols += (colspan != null) ? parseInt(colspan) : 1; } var row = bd.insertRow(index); for (var i = 0; i < cols; i++) { mxUtils.br(row.insertCell(-1)); } return row.cells[0]; }; /** * Deletes the given column. */ Graph.prototype.deleteRow = function(table, index) { table.tBodies[0].deleteRow(index); }; /** * Deletes the given column. */ Graph.prototype.insertColumn = function(table, index) { var hd = table.tHead; if (hd != null) { // TODO: use colIndex for (var h = 0; h < hd.rows.length; h++) { var th = document.createElement('th'); hd.rows[h].appendChild(th); mxUtils.br(th); } } var bd = table.tBodies[0]; for (var i = 0; i < bd.rows.length; i++) { var cell = bd.rows[i].insertCell(index); mxUtils.br(cell); } return bd.rows[0].cells[(index >= 0) ? index : bd.rows[0].cells.length - 1]; }; /** * Deletes the given column. */ Graph.prototype.deleteColumn = function(table, index) { if (index >= 0) { var bd = table.tBodies[0]; var rows = bd.rows; for (var i = 0; i < rows.length; i++) { if (rows[i].cells.length > index) { rows[i].deleteCell(index); } } } }; /** * Inserts the given HTML at the caret position (no undo). */ Graph.prototype.pasteHtmlAtCaret = function(html) { var sel, range; // IE9 and non-IE if (window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); range.deleteContents(); // Range.createContextualFragment() would be useful here but is // only relatively recently standardized and is not supported in // some browsers (IE9, for one) var el = document.createElement("div"); el.innerHTML = html; var frag = document.createDocumentFragment(), node; while ((node = el.firstChild)) { lastNode = frag.appendChild(node); } range.insertNode(frag); } } // IE < 9 else if ((sel = document.selection) && sel.type != "Control") { // FIXME: Does not work if selection is empty sel.createRange().pasteHTML(html); } }; /** * Creates an anchor elements for handling the given link in the * hint that is shown when the cell is selected. */ Graph.prototype.createLinkForHint = function(link, label) { link = (link != null) ? link : 'javascript:void(0);'; if (label == null || label.length == 0) { if (this.isCustomLink(link)) { label = this.getLinkTitle(link); } else { label = link; } } // Helper function to shorten strings function short(str, max) { if (str.length > max) { str = str.substring(0, Math.round(max / 2)) + '...' + str.substring(str.length - Math.round(max / 4)); } return str; }; var a = document.createElement('a'); a.setAttribute('rel', this.linkRelation); a.setAttribute('href', this.getAbsoluteUrl(link)); a.setAttribute('title', short((this.isCustomLink(link)) ? this.getLinkTitle(link) : link, 80)); if (this.linkTarget != null) { a.setAttribute('target', this.linkTarget); } // Adds shortened label to link mxUtils.write(a, short(label, 40)); // Handles custom links if (this.isCustomLink(link)) { mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt) { this.customLinkClicked(link); mxEvent.consume(evt); })); } return a; }; /** * Customized graph for touch devices. */ Graph.prototype.initTouch = function() { // Disables new connections via "hotspot" this.connectionHandler.marker.isEnabled = function() { return this.graph.connectionHandler.first != null; }; // Hides menu when editing starts this.addListener(mxEvent.START_EDITING, function(sender, evt) { this.popupMenuHandler.hideMenu(); }); // Adds custom hit detection if native hit detection found no cell var graphUpdateMouseEvent = this.updateMouseEvent; this.updateMouseEvent = function(me) { me = graphUpdateMouseEvent.apply(this, arguments); if (mxEvent.isTouchEvent(me.getEvent()) && me.getState() == null) { var cell = this.getCellAt(me.graphX, me.graphY); if (cell != null && this.isSwimlane(cell) && this.hitsSwimlaneContent(cell, me.graphX, me.graphY)) { cell = null; } else { me.state = this.view.getState(cell); if (me.state != null && me.state.shape != null) { this.container.style.cursor = me.state.shape.node.style.cursor; } } } if (me.getState() == null && this.isEnabled()) { this.container.style.cursor = 'default'; } return me; }; // Context menu trigger implementation depending on current selection state // combined with support for normal popup trigger. var cellSelected = false; var selectionEmpty = false; var menuShowing = false; var oldFireMouseEvent = this.fireMouseEvent; this.fireMouseEvent = function(evtName, me, sender) { if (evtName == mxEvent.MOUSE_DOWN) { // For hit detection on edges me = this.updateMouseEvent(me); cellSelected = this.isCellSelected(me.getCell()); selectionEmpty = this.isSelectionEmpty(); menuShowing = this.popupMenuHandler.isMenuShowing(); } oldFireMouseEvent.apply(this, arguments); }; // Shows popup menu if cell was selected or selection was empty and background was clicked // FIXME: Conflicts with mxPopupMenuHandler.prototype.getCellForPopupEvent in Editor.js by // selecting parent for selected children in groups before this check can be made. this.popupMenuHandler.mouseUp = mxUtils.bind(this, function(sender, me) { this.popupMenuHandler.popupTrigger = !this.isEditing() && this.isEnabled() && (me.getState() == null || !me.isSource(me.getState().control)) && (this.popupMenuHandler.popupTrigger || (!menuShowing && !mxEvent.isMouseEvent(me.getEvent()) && ((selectionEmpty && me.getCell() == null && this.isSelectionEmpty()) || (cellSelected && this.isCellSelected(me.getCell()))))); mxPopupMenuHandler.prototype.mouseUp.apply(this.popupMenuHandler, arguments); }); }; /** * HTML in-place editor */ mxCellEditor.prototype.isContentEditing = function() { var state = this.graph.view.getState(this.editingCell); return state != null && state.style['html'] == 1; }; /** * Returns true if all selected text is inside a table element. */ mxCellEditor.prototype.isTableSelected = function() { return this.graph.getParentByName( this.graph.getSelectedElement(), 'TABLE', this.textarea) != null; }; /** * Sets the alignment of the current selected cell. This sets the * alignment in the cell style, removes all alignment within the * text and invokes the built-in alignment function. * * Only the built-in function is invoked if shift is pressed or * if table cells are selected and shift is not pressed. */ mxCellEditor.prototype.alignText = function(align, evt) { var shiftPressed = evt != null && mxEvent.isShiftDown(evt); if (shiftPressed || (window.getSelection != null && window.getSelection().containsNode != null)) { var allSelected = true; this.graph.processElements(this.textarea, function(node) { if (shiftPressed || window.getSelection().containsNode(node, true)) { node.removeAttribute('align'); node.style.textAlign = null; } else { allSelected = false; } }); if (allSelected) { this.graph.cellEditor.setAlign(align); } } document.execCommand('justify' + align.toLowerCase(), false, null); }; /** * Creates the keyboard event handler for the current graph and history. */ mxCellEditor.prototype.saveSelection = function() { if (window.getSelection) { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { var ranges = []; for (var i = 0, len = sel.rangeCount; i < len; ++i) { ranges.push(sel.getRangeAt(i)); } return ranges; } } else if (document.selection && document.selection.createRange) { return document.selection.createRange(); } return null; }; /** * Creates the keyboard event handler for the current graph and history. */ mxCellEditor.prototype.restoreSelection = function(savedSel) { try { if (savedSel) { if (window.getSelection) { sel = window.getSelection(); sel.removeAllRanges(); for (var i = 0, len = savedSel.length; i < len; ++i) { sel.addRange(savedSel[i]); } } else if (document.selection && savedSel.select) { savedSel.select(); } } } catch (e) { // ignore } }; /** * Handling of special nl2Br style for not converting newlines to breaks in HTML labels. * NOTE: Since it's easier to set this when the label is created we assume that it does * not change during the lifetime of the mxText instance. */ var mxCellRendererInitializeLabel = mxCellRenderer.prototype.initializeLabel; mxCellRenderer.prototype.initializeLabel = function(state) { if (state.text != null) { state.text.replaceLinefeeds = mxUtils.getValue(state.style, 'nl2Br', '1') != '0'; } mxCellRendererInitializeLabel.apply(this, arguments); }; var mxConstraintHandlerUpdate = mxConstraintHandler.prototype.update; mxConstraintHandler.prototype.update = function(me, source) { if (this.isKeepFocusEvent(me) || !mxEvent.isAltDown(me.getEvent())) { mxConstraintHandlerUpdate.apply(this, arguments); } else { this.reset(); } }; /** * No dashed shapes. */ mxGuide.prototype.createGuideShape = function(horizontal) { var guide = new mxPolyline([], mxConstants.GUIDE_COLOR, mxConstants.GUIDE_STROKEWIDTH); return guide; }; /** * HTML in-place editor */ mxCellEditor.prototype.escapeCancelsEditing = false; var mxCellEditorStartEditing = mxCellEditor.prototype.startEditing; mxCellEditor.prototype.startEditing = function(cell, trigger) { mxCellEditorStartEditing.apply(this, arguments); // Overrides class in case of HTML content to add // dashed borders for divs and table cells var state = this.graph.view.getState(cell); if (state != null && state.style['html'] == 1) { this.textarea.className = 'mxCellEditor geContentEditable'; } else { this.textarea.className = 'mxCellEditor mxPlainTextEditor'; } // Toggles markup vs wysiwyg mode this.codeViewMode = false; // Stores current selection range when switching between markup and code this.switchSelectionState = null; // Selects editing cell this.graph.setSelectionCell(cell); // Enables focus outline for edges and edge labels var parent = this.graph.getModel().getParent(cell); var geo = this.graph.getCellGeometry(cell); if ((this.graph.getModel().isEdge(parent) && geo != null && geo.relative) || this.graph.getModel().isEdge(cell)) { // Quirks does not support outline at all so use border instead if (mxClient.IS_QUIRKS) { this.textarea.style.border = 'gray dotted 1px'; } // IE>8 and FF on Windows uses outline default of none else if (mxClient.IS_IE || mxClient.IS_IE11 || (mxClient.IS_FF && mxClient.IS_WIN)) { this.textarea.style.outline = 'gray dotted 1px'; } else { this.textarea.style.outline = ''; } } else if (mxClient.IS_QUIRKS) { this.textarea.style.outline = 'none'; this.textarea.style.border = ''; } } /** * HTML in-place editor */ var cellEditorInstallListeners = mxCellEditor.prototype.installListeners; mxCellEditor.prototype.installListeners = function(elt) { cellEditorInstallListeners.apply(this, arguments); // Adds a reference from the clone to the original node, recursively function reference(node, clone) { clone.originalNode = node; node = node.firstChild; var child = clone.firstChild; while (node != null && child != null) { reference(node, child); node = node.nextSibling; child = child.nextSibling; } return clone; }; // Checks the given node for new nodes, recursively function checkNode(node, clone) { if (node != null) { if (clone.originalNode != node) { cleanNode(node); } else { node = node.firstChild; clone = clone.firstChild; while (node != null) { var nextNode = node.nextSibling; if (clone == null) { cleanNode(node); } else { checkNode(node, clone); clone = clone.nextSibling; } node = nextNode; } } } }; // Removes unused DOM nodes and attributes, recursively function cleanNode(node) { var child = node.firstChild; while (child != null) { var next = child.nextSibling; cleanNode(child); child = next; } if ((node.nodeType != 1 || (node.nodeName !== 'BR' && node.firstChild == null)) && (node.nodeType != 3 || mxUtils.trim(mxUtils.getTextContent(node)).length == 0)) { node.parentNode.removeChild(node); } else { // Removes linefeeds if (node.nodeType == 3) { mxUtils.setTextContent(node, mxUtils.getTextContent(node).replace(/\n|\r/g, '')); } // Removes CSS classes and styles (for Word and Excel) if (node.nodeType == 1) { node.removeAttribute('style'); node.removeAttribute('class'); node.removeAttribute('width'); node.removeAttribute('cellpadding'); node.removeAttribute('cellspacing'); node.removeAttribute('border'); } } }; // Handles paste from Word, Excel etc by removing styles, classnames and unused nodes // LATER: Fix undo/redo for paste if (!mxClient.IS_QUIRKS && document.documentMode !== 7 && document.documentMode !== 8) { mxEvent.addListener(this.textarea, 'paste', mxUtils.bind(this, function(evt) { var clone = reference(this.textarea, this.textarea.cloneNode(true)); window.setTimeout(mxUtils.bind(this, function() { // Paste from Word or Excel if (this.textarea != null && (this.textarea.innerHTML.indexOf('') >= 0 || this.textarea.innerHTML.indexOf('