UNPKG

drawio-offline

Version:
1,677 lines (1,430 loc) 338 kB
/** * 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(); }; } // Polyfill for Uint8Array.from in IE11 used in Graph.decompress // See https://stackoverflow.com/questions/36810940/alternative-or-polyfill-for-array-from-on-the-internet-explorer if (!Uint8Array.from) { Uint8Array.from = (function () { var toStr = Object.prototype.toString; var isCallable = function (fn) { return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; }; var toInteger = function (value) { var number = Number(value); if (isNaN(number)) { return 0; } if (number === 0 || !isFinite(number)) { return number; } return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); }; var maxSafeInteger = Math.pow(2, 53) - 1; var toLength = function (value) { var len = toInteger(value); return Math.min(Math.max(len, 0), maxSafeInteger); }; // The length property of the from method is 1. return function from(arrayLike/*, mapFn, thisArg */) { // 1. Let C be the this value. var C = this; // 2. Let items be ToObject(arrayLike). var items = Object(arrayLike); // 3. ReturnIfAbrupt(items). if (arrayLike == null) { throw new TypeError("Array.from requires an array-like object - not null or undefined"); } // 4. If mapfn is undefined, then let mapping be false. var mapFn = arguments.length > 1 ? arguments[1] : void undefined; var T; if (typeof mapFn !== 'undefined') { // 5. else // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. if (!isCallable(mapFn)) { throw new TypeError('Array.from: when provided, the second argument must be a function'); } // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. if (arguments.length > 2) { T = arguments[2]; } } // 10. Let lenValue be Get(items, "length"). // 11. Let len be ToLength(lenValue). var len = toLength(items.length); // 13. If IsConstructor(C) is true, then // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. // 14. a. Else, Let A be ArrayCreate(len). var A = isCallable(C) ? Object(new C(len)) : new Array(len); // 16. Let k be 0. var k = 0; // 17. Repeat, while k < len… (also steps a - h) var kValue; while (k < len) { kValue = items[k]; if (mapFn) { A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); } else { A[k] = kValue; } k += 1; } // 18. Let putStatus be Put(A, "length", len, true). A.length = len; // 20. Return A. return A; }; }()); } // 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.defaultDarkGridColor = '#6e6e6e'; mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultGridColor; //Units mxGraphView.prototype.unit = mxConstants.POINTS; mxGraphView.prototype.setUnit = function(unit) { if (this.unit != unit) { this.unit = unit; this.fireEvent(new mxEventObject('unitChanged', 'unit', unit)); } }; // Alternative text for unsupported foreignObjects mxSvgCanvas2D.prototype.foAltText = '[Not supported by viewer]'; // Hook for custom constraints mxShape.prototype.getConstraints = function(style, w, h) { return null; }; // Override for clipSvg style. mxImageShape.prototype.getImageDataUri = function() { var src = this.image; if (src.substring(0, 26) == 'data:image/svg+xml;base64,' && this.style != null && mxUtils.getValue(this.style, 'clipSvg', '0') == '1') { if (this.clippedSvg == null || this.clippedImage != src) { this.clippedSvg = Graph.clipSvgDataUri(src); this.clippedImage = src; } src = this.clippedSvg; } return src; }; /** * 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'); var state = me.getState(); if (!mxEvent.isAltDown(me.getEvent()) && 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); } } } else if (!this.panningHandler.isActive() && !mxEvent.isControlDown(me.getEvent())) { var handler = this.selectionCellsHandler.getHandler(state.cell); // Cell handles have precedence over row and col resize if (handler == null || handler.getHandleForEvent(me) == null) { var box = new mxRectangle(me.getGraphX() - 1, me.getGraphY() - 1); box.grow(mxEvent.isTouchEvent(me.getEvent()) ? mxShape.prototype.svgStrokeTolerance - 1 : (mxShape.prototype.svgStrokeTolerance + 1) / 2); if (this.isTableCell(state.cell) && !this.isCellSelected(state.cell)) { var row = this.model.getParent(state.cell); var table = this.model.getParent(row); if (!this.isCellSelected(table)) { if ((mxUtils.intersects(box, new mxRectangle(state.x, state.y - 2, state.width, 3)) && this.model.getChildAt(table, 0) != row) || mxUtils.intersects(box, new mxRectangle( state.x, state.y + state.height - 2, state.width, 3)) || (mxUtils.intersects(box, new mxRectangle(state.x - 2, state.y, 2, state.height)) && this.model.getChildAt(row, 0) != state.cell) || mxUtils.intersects(box, new mxRectangle( state.x + state.width - 2, state.y, 2, state.height))) { var wasSelected = this.selectionCellsHandler.isHandled(table); this.selectCellForEvent(table, me.getEvent()); handler = this.selectionCellsHandler.getHandler(table); if (handler != null) { var handle = handler.getHandleForEvent(me); if (handle != null) { handler.start(me.getGraphX(), me.getGraphY(), handle); handler.blockDelayedSelection = !wasSelected; me.consume(); } } } } } // Hover for swimlane start sizes inside tables var current = state; while (!me.isConsumed() && current != null && (this.isTableCell(current.cell) || this.isTableRow(current.cell) || this.isTable(current.cell))) { if (this.isSwimlane(current.cell)) { var offset = this.getActualStartSize(current.cell); var s = this.view.scale; if (((offset.x > 0 || offset.width > 0) && mxUtils.intersects(box, new mxRectangle( current.x + (offset.x - offset.width - 1) * s + ((offset.x == 0) ? current.width : 0), current.y, 1, current.height))) || ((offset.y > 0 || offset.height > 0) && mxUtils.intersects(box, new mxRectangle(current.x, current.y + (offset.y - offset.height - 1) * s + ((offset.y == 0) ? current.height : 0), current.width, 1)))) { this.selectCellForEvent(current.cell, me.getEvent()); handler = this.selectionCellsHandler.getHandler(current.cell); if (handler != null) { // Swimlane start size handle is last custom handle var handle = mxEvent.CUSTOM_HANDLE - handler.customHandles.length + 1; handler.start(me.getGraphX(), me.getGraphY(), handle); me.consume(); } } } current = this.view.getState(this.model.getParent(current.cell)); } } } } } })); 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.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) { var handler = this.selectionCellsHandler.getHandler(state.cell); if (handler == null && this.model.isEdge(state.cell)) { handler = this.createHandler(state); } 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); 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(); } } if (handler != null) { // Lazy selection for edges inside groups if (this.selectionCellsHandler.isHandlerActive(handler)) { if (!this.isCellSelected(state.cell)) { this.selectionCellsHandler.handlers.put(state.cell, handler); this.selectCellForEvent(state.cell, me.getEvent()); } } else if (!this.isCellSelected(state.cell)) { // Destroy temporary handler handler.destroy(); } } // Reset start state start.selected = false; start.handle = null; start.state = null; start.event = null; start.point = null; } } else { // Updates cursor for unselected edges under the mouse var state = me.getState(); if (state != null) { var cursor = null; // Checks if state was removed in call to stopEditing above if (this.model.isEdge(state.cell)) { var box = new mxRectangle(me.getGraphX(), me.getGraphY()); box.grow(mxEdgeHandler.prototype.handleImage.width / 2); var pts = state.absolutePoints; if (pts != null) { if (state.text != null && state.text.boundingBox != null && mxUtils.contains(state.text.boundingBox, me.getGraphX(), me.getGraphY())) { cursor = 'move'; } else if (mxUtils.contains(box, pts[0].x, pts[0].y) || mxUtils.contains(box, pts[pts.length - 1].x, pts[pts.length - 1].y)) { cursor = 'pointer'; } else 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'; } } } } } else if (!mxEvent.isControlDown(me.getEvent())) { var box = new mxRectangle(me.getGraphX() - 1, me.getGraphY() - 1); box.grow(mxShape.prototype.svgStrokeTolerance / 2); if (this.isTableCell(state.cell)) { var row = this.model.getParent(state.cell); var table = this.model.getParent(row); if (!this.isCellSelected(table)) { if ((mxUtils.intersects(box, new mxRectangle(state.x - 2, state.y, 2, state.height)) && this.model.getChildAt(row, 0) != state.cell) || mxUtils.intersects(box, new mxRectangle(state.x + state.width - 2, state.y, 2, state.height))) { cursor ='col-resize'; } else if ((mxUtils.intersects(box, new mxRectangle(state.x, state.y - 2, state.width, 3)) && this.model.getChildAt(table, 0) != row) || mxUtils.intersects(box, new mxRectangle(state.x, state.y + state.height - 2, state.width, 3))) { cursor ='row-resize'; } } } // Hover for swimlane start sizes inside tables var current = state; while (cursor == null && current != null && (this.isTableCell(current.cell) || this.isTableRow(current.cell) || this.isTable(current.cell))) { if (this.isSwimlane(current.cell)) { var offset = this.getActualStartSize(current.cell); var s = this.view.scale; if ((offset.x > 0 || offset.width > 0) && mxUtils.intersects(box, new mxRectangle( current.x + (offset.x - offset.width - 1) * s + ((offset.x == 0) ? current.width * s : 0), current.y, 1, current.height))) { cursor ='col-resize'; } else if ((offset.y > 0 || offset.height > 0) && mxUtils.intersects(box, new mxRectangle( current.x, current.y + (offset.y - offset.height - 1) * s + ((offset.y == 0) ? current.height : 0), current.width, 1))) { cursor ='row-resize'; } } current = this.view.getState(this.model.getParent(current.cell)); } } if (cursor != null) { state.setCursor(cursor); } } } } }), mouseUp: mxUtils.bind(this, function(sender, me) { start.state = null; start.event = null; start.point = null; start.handle = null; }) }); } // 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 lookup = new mxDictionary(); var newCells = []; for (var i = 0; i < cells.length; i++) { // Propagates to composite parents or moves selected table rows var cell = (this.graph.isTableCell(initialCell) && this.graph.isTableCell(cells[i]) && this.graph.isCellSelected(cells[i])) ? this.graph.model.getParent(cells[i]) : ((this.graph.isTableRow(initialCell) && this.graph.isTableRow(cells[i]) && this.graph.isCellSelected(cells[i])) ? cells[i] : this.graph.getCompositeParent(cells[i])); if (cell != null && !lookup.get(cell)) { lookup.put(cell, true); newCells.push(cell); } } return newCells; }; // Handles parts and selected rows in tables of cells for drag and drop var graphHandlerStart = this.graphHandler.start; this.graphHandler.start = function(cell, x, y, cells) { // Propagates to selected table row to start move var ignoreParent = false; if (this.graph.isTableCell(cell)) { if (!this.graph.isCellSelected(cell)) { cell = this.graph.model.getParent(cell); } else { ignoreParent = true; } } if (!ignoreParent && (!this.graph.isTableRow(cell) || !this.graph.isCellSelected(cell))) { cell = this.graph.getCompositeParent(cell); } graphHandlerStart.apply(this, arguments); }; // Handles parts of cells when cloning the source for new connections this.connectionHandler.createTargetVertex = function(evt, source) { source = this.graph.getCompositeParent(source); return mxConnectionHandler.prototype.createTargetVertex.apply(this, arguments); }; 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; }; // Opens links in tooltips in new windows var tooltipHandlerShow = this.tooltipHandler.show; this.tooltipHandler.show = function() { tooltipHandlerShow.apply(this, arguments); if (this.div != null) { var links = this.div.getElementsByTagName('a'); for (var i = 0; i < links.length; i++) { if (links[i].getAttribute('href') != null && links[i].getAttribute('target') == null) { links[i].setAttribute('target', '_blank'); } } } }; // Redirects tooltips for locked cells this.tooltipHandler.getStateForEvent = function(me) { return me.sourceState; }; // Redirects cursor for locked cells var getCursorForMouseEvent = this.getCursorForMouseEvent; this.getCursorForMouseEvent = function(me) { var locked = me.state == null && me.sourceState != null && this.isCellLocked(me.sourceState.cell); return this.getCursorForCell((locked) ? me.sourceState.cell : me.getCell()); }; // Shows pointer cursor for clickable cells with links // ie. if the graph is disabled and cells cannot be selected var getCursorForCell = this.getCursorForCell; this.getCursorForCell = function(cell) { if (!this.isEnabled() || this.isCellLocked(cell)) { var link = this.getClickableLinkForCell(cell); if (link != null) { return 'pointer'; } else if (this.isCellLocked(cell)) { return 'default'; } } return getCursorForCell.apply(this, arguments); }; // Changes rubberband selection ignore locked cells this.selectRegion = function(rect, evt) { var cells = this.getCells(rect.x, rect.y, rect.width, rect.height, null, null, null, function(state) { return mxUtils.getValue(state.style, 'locked', '0') == '1'; }, true); this.selectCellsForEvent(cells, evt); return cells; }; // Never removes cells from parents that are being moved var graphHandlerShouldRemoveCellsFromParent = this.graphHandler.shouldRemoveCellsFromParent; this.graphHandler.shouldRemoveCellsFromParent = function(parent, cells, evt) { if (this.graph.isCellSelected(parent)) { return false; } return graphHandlerShouldRemoveCellsFromParent.apply(this, arguments); }; // Unlocks all cells this.isCellLocked = function(cell) { while (cell != null) { if (mxUtils.getValue(this.getCurrentCellStyle(cell), 'locked', '0') == '1') { return true; } cell = this.model.getParent(cell); } return false; }; var tapAndHoldSelection = null; // Uses this event to process mouseDown to check the selection state before it is changed this.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt) { if (evt.getProperty('eventName') == 'mouseDown') { var me = evt.getProperty('event'); var state = me.getState(); if (state != null && !this.isSelectionEmpty() && !this.isCellSelected(state.cell)) { tapAndHoldSelection = this.getSelectionCells(); } else { tapAndHoldSelection = null; } } })); // Tap and hold on background starts rubberband for multiple selected // cells the cell associated with the event is deselected this.addListener(mxEvent.TAP_AND_HOLD, mxUtils.bind(this, function(sender, evt) { if (!mxEvent.isMultiTouchEvent(evt)) { var me = evt.getProperty('event'); var cell = evt.getProperty('cell'); if (cell == null) { var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(me), mxEvent.getClientY(me)); rubberband.start(pt.x, pt.y); } else if (tapAndHoldSelection != null) { this.addSelectionCells(tapAndHoldSelection); } else if (this.getSelectionCount() > 1 && this.isCellSelected(cell)) { this.removeSelectionCell(cell); } // Blocks further processing of the event tapAndHoldSelection = null; evt.consume(); } })); // On connect the target is selected and we clone the cell of the preview edge for insert this.connectionHandler.selectCells = function(edge, target) { this.graph.setSelectionCell(target || edge); }; // Shows connection points only if cell not selected and parent table not handled this.connectionHandler.constraintHandler.isStateIgnored = function(state, source) { var graph = state.view.graph; return source && (graph.isCellSelected(state.cell) || (graph.isTableRow(state.cell) && graph.selectionCellsHandler.isHandled(graph.model.getParent(state.cell)))); }; // Updates constraint handler if the selection changes this.selectionModel.addListener(mxEvent.CHANGE, mxUtils.bind(this, function() { var ch = this.connectionHandler.constraintHandler; if (ch.currentFocus != null && ch.isStateIgnored(ch.currentFocus, true)) { ch.currentFocus = null; ch.constraints = null; ch.destroyIcons(); } ch.destroyFocusHighlight(); })); // Initializes touch interface if (Graph.touchStyle) { this.initTouch(); } /** * 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'); /** * Shortcut for capability check. */ Graph.translateDiagram = urlParams['translate-diagram'] == '1'; /** * Shortcut for capability check. */ Graph.diagramLanguage = (urlParams['diagram-language'] != null) ? urlParams['diagram-language'] : mxClient.language; /** * Default size for line jumps. */ Graph.lineJumpsEnabled = true; /** * Default size for line jumps. */ Graph.defaultJumpSize = 6; /** * 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://www.diagrams.net/doc/faq/svg-export-text-problems'; /** * Minimum height for table rows. */ Graph.pasteStyles = ['rounded', 'shadow', 'dashed', 'dashPattern', 'fontFamily', 'fontSource', 'fontSize', 'fontColor', 'fontStyle', 'align', 'verticalAlign', 'strokeColor', 'strokeWidth', 'fillColor', 'gradientColor', 'swimlaneFillColor', 'textOpacity', 'gradientDirection', 'glass', 'labelBackgroundColor', 'labelBorderColor', 'opacity', 'spacing', 'spacingTop', 'spacingLeft', 'spacingBottom', 'spacingRight', 'endFill', 'endArrow', 'endSize', 'targetPerimeterSpacing', 'startFill', 'startArrow', 'startSize', 'sourcePerimeterSpacing', 'arcSize', 'comic', 'sketch', 'fillWeight', 'hachureGap', 'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle', 'curveFitting', 'simplification', 'comicStyle']; /** * Helper function for creating SVG data URI. */ Graph.createSvgImage = function(w, h, data, coordWidth, coordHeight) { var tmp = unescape(encodeURIComponent( '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + w + 'px" height="' + h + 'px" ' + ((coordWidth != null && coordHeight != null) ? 'viewBox="0 0 ' + coordWidth + ' ' + coordHeight + '" ' : '') + 'version="1.1">' + data + '</svg>')); 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 lastIndex = 0; var checked = []; for (var i = 0; i < text.length; i++) { var code = text.charCodeAt(i); // Removes all control chars except TAB, LF and CR if (!((code >= 32 || code == 9 || code == 10 || code == 13) && code != 0xFFFF && code != 0xFFFE)) { checked.push(text.substring(lastIndex, i)); lastIndex = i + 1; } } if (lastIndex > 0 && lastIndex < text.length) { checked.push(text.substring(lastIndex)); } return (checked.length == 0) ? text : checked.join(''); }; /** * Turns the given string into an array. */ Graph.stringToBytes = function(str) { var arr = new Array(str.length); for (var i = 0; i < str.length; i++) { arr[i] = str.charCodeAt(i); } return arr; }; /** * Turns the given array into a string. */ Graph.bytesToString = function(arr) { var result = new Array(arr.length); for (var i = 0; i < arr.length; i++) { result[i] = String.fromCharCode(arr[i]); } return result.join(''); }; /** * Turns the given array into a string. */ Graph.base64EncodeUnicode = function(str) { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { return String.fromCharCode(parseInt(p1, 16)) })); }; /** * Turns the given array into a string. */ Graph.base64DecodeUnicode = function(str) { return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) }).join('')); }; /** * Returns a base64 encoded version of the compressed outer XML of the given node. */ Graph.compressNode = function(node, checked) { var xml = mxUtils.getXml(node); return Graph.compress((checked) ? xml : Graph.zapGremlins(xml)); }; /** * Returns a string for the given array buffer. */ Graph.arrayBufferToString = function(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return binary; }; /** * Returns an array buffer for the given string. */ Graph.stringToArrayBuffer = function(data) { return Uint8Array.from(data, function (c) { return c.charCodeAt(0); }); }; /** * Returns index of a string in an array buffer (UInt8Array) */ Graph.arrayBufferIndexOfString = function (uint8Array, str, start) { var c0 = str.charCodeAt(0), j = 1, p = -1; //Index of first char for (var i = start || 0; i < uint8Array.byteLength; i++) { if (uint8Array[i] == c0) { p = i; break; } } for (var i = p + 1; p > -1 && i < uint8Array.byteLength && i < p + str.length - 1; i++) { if (uint8Array[i] != str.charCodeAt(j)) { return Graph.arrayBufferIndexOfString(uint8Array, str, p + 1); } j++; } return j == str.length - 1? p : -1; }; /** * Returns a base64 encoded version of the compressed string. */ Graph.compress = function(data, deflate) { if (data == null || data.length == 0 || typeof(pako) === 'undefined') { return data; } else { var tmp = (deflate) ? pako.deflate(encodeURIComponent(data)) : pako.deflateRaw(encodeURIComponent(data)); return btoa(Graph.arrayBufferToString(new Uint8Array(tmp))); } }; /** * Returns a decompressed version of the base64 encoded string. */ Graph.decompress = function(data, inflate, checked) { if (data == null || data.length == 0 || typeof(pako) === 'undefined') { return data; } else { var tmp = Graph.stringToArrayBuffer(atob(data)); var inflated = decodeURIComponent((inflate) ? pako.inflate(tmp, {to: 'string'}) : pako.inflateRaw(tmp, {to: 'string'})); return (checked) ? inflated : Graph.zapGremlins(inflated); } }; /** * Removes formatting from pasted HTML. */ Graph.removePasteFormatting = function(elt) { while (elt != null) { if (elt.firstChild != null) { Graph.removePasteFormatting(elt.firstChild); } if (elt.nodeType == mxConstants.NODETYPE_ELEMENT && elt.style != null) { elt.style.whiteSpace = ''; if (elt.style.color == '#000000') { elt.style.color = ''; } } elt = elt.nextSibling; } }; /** * Sanitizes the given HTML markup. */ Graph.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); }; /** * Removes all script tags and attributes starting with on. */ Graph.sanitizeSvg = function(div) { // Removes all attributes starting with on var all = div.getElementsByTagName('*'); for (var i = 0; i < all.length; i++) { for (var j = 0; j < all[i].attributes.length; j++) { var attr = all[i].attributes[j]; if (attr.name.length > 2 && attr.name.toLowerCase().substring(0, 2) == 'on') { all[i].removeAttribute(attr.name); } } } // Removes all script tags var scripts = div.getElementsByTagName('script'); while (scripts.length > 0) { scripts[0].parentNode.removeChild(scripts[0]); } }; /** * Updates the viewbox, width and height in the given SVG data URI * and returns the updated data URI with all script tags and event * handlers removed. */ Graph.clipSvgDataUri = function(dataUri) { // LATER Add workaround for non-default NS declarations with empty URI not allowed in IE11 if (!mxClient.IS_IE && !mxClient.IS_IE11 && dataUri != null && dataUri.substring(0, 26) == 'data:image/svg+xml;base64,') { try { var div = document.createElement('div'); div.style.position = 'absolute'; div.style.visibility = 'hidden'; // Adds the text and inserts into DOM for updating of size var data = decodeURIComponent(escape(atob(dataUri.substring(26)))); var idx = data.indexOf('<svg'); if (idx >= 0) { // Strips leading XML declaration and doctypes div.innerHTML = data.substring(idx); // Removes all attributes starting with on Graph.sanitizeSvg(div); // Gets the size and removes from DOM var svgs = div.getElementsByTagName('svg'); if (svgs.length > 0) { document.body.appendChild(div); try { var size = svgs[0].getBBox(); if (size.width > 0 && size.height > 0) { div.getElementsByTagName('svg')[0].setAttribute('viewBox', size.x + ' ' + size.y + ' ' + size.width + ' ' + size.height); div.getElementsByTagName('svg')[0].setAttribute('width', size.width); div.getElementsByTagName('svg')[0].setAttribute('height', size.height); } } catch (e) { // ignore } finally { document.body.removeChild(div); } dataUri = Editor.createSvgDataUri(mxUtils.getXml(svgs[0])); } } } catch (e) { // ignore } } return dataUri; }; /** * Returns the CSS font family from the given computed style. */ Graph.stripQuotes = function(text) { if (text != null) { if (text.charAt(0) == '\'') { text = text.substring(1); } if (text.charAt(text.length - 1) == '\'') { text = text.substring(0, text.length - 1); } if (text.charAt(0) == '"') { text = text.substring(1); } if (text.charAt(text.length - 1)