UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

479 lines (442 loc) 19.8 kB
"use strict"; /* * Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsTS folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ /** * @constructor * @extends DraggingTool * @class * This draggingTool class makes guidelines visible as the parts are dragged around a diagram * when the selected part is nearly aligned with another part. */ function GuidedDraggingTool() { go.DraggingTool.call(this); // temporary parts for horizonal guidelines var $ = go.GraphObject.make; var partProperties = { layerName: "Tool", isInDocumentBounds: false }; var shapeProperties = { stroke: "gray", isGeometryPositioned: true }; /** @ignore */ this.guidelineHtop = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" })); /** @ignore */ this.guidelineHbottom = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" })); /** @ignore */ this.guidelineHcenter = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" })); // temporary parts for vertical guidelines /** @ignore */ this.guidelineVleft = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" })); /** @ignore */ this.guidelineVright = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" })); /** @ignore */ this.guidelineVcenter = $(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" })); // properties that the programmer can modify /** @type {number} */ this._guidelineSnapDistance = 6; /** @type {boolean} */ this._isGuidelineEnabled = true; /** @type {string} */ this._horizontalGuidelineColor = "gray"; /** @type {string} */ this._verticalGuidelineColor = "gray"; /** @type {string} */ this._centerGuidelineColor = "gray"; /** @type {number} */ this._guidelineWidth = 1; /** @type {number} */ this._searchDistance = 1000; /** @type {boolean} */ this._isGuidelineSnapEnabled = true; } go.Diagram.inherit(GuidedDraggingTool, go.DraggingTool); /** * Removes all of the guidelines from the grid. * @this {GuidedDraggingTool} */ GuidedDraggingTool.prototype.clearGuidelines = function() { this.diagram.remove(this.guidelineHbottom); this.diagram.remove(this.guidelineHcenter); this.diagram.remove(this.guidelineHtop); this.diagram.remove(this.guidelineVleft); this.diagram.remove(this.guidelineVright); this.diagram.remove(this.guidelineVcenter); } /** * Calls the base method from {@link DraggingTool#doDeactivate} * and removes the guidelines from the graph. * @this {GuidedDraggingTool} */ GuidedDraggingTool.prototype.doDeactivate = function() { go.DraggingTool.prototype.doDeactivate.call(this); // clear any guidelines when dragging is done this.clearGuidelines(); }; GuidedDraggingTool.prototype.doDragOver = function(pt, obj) { // clear all existing guidelines in case either show... method decides to show a guideline this.clearGuidelines(); // gets the selected part var partItr = (this.copiedParts || this.draggedParts).iterator; if (partItr.next()) { var part = partItr.key; this.showHorizontalMatches(part, this.isGuidelineEnabled, false); this.showVerticalMatches(part, this.isGuidelineEnabled, false); } } /** * On a mouse-up, snaps the selected part to the nearest guideline. * If not snapping, the part remains at its position. * This calls {@link #guidelineSnap}. * @this {GuidedDraggingTool} */ GuidedDraggingTool.prototype.doDropOnto = function(pt, obj) { this.clearGuidelines(); // gets the selected (perhaps copied) Part var partItr = (this.copiedParts || this.draggedParts).iterator; if (partItr.next()) { var part = partItr.key; // snaps only when the mouse is released without shift modifier var e = this.diagram.lastInput; var snap = this.isGuidelineSnapEnabled && !e.shift; this.showHorizontalMatches(part, false, snap); // false means don't show guidelines this.showVerticalMatches(part, false, snap); } } /** * When nodes are shifted due to being guided upon a drop, make sure all connected link routes are invalidated, * since the node is likely to have moved a different amount than all its connected links in the regular * operation of the DraggingTool. * @this {GuidedDraggingTool} */ GuidedDraggingTool.prototype.invalidateLinks = function(node) { if (node instanceof go.Node) node.invalidateConnectedLinks(); } /** * This finds parts that are aligned near the selected part along horizontal lines. It compares the selected * part to all parts within a rectangle approximately twice the {@link #searchDistance} wide. * The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}. * The parameters used for {@link #guidelineSnap} are also set here. * @this {GuidedDraggingTool} * @param {Part} part * @param {boolean} guideline if true, show guideline * @param {boolean} snap if true, snap the part to where the guideline would be */ GuidedDraggingTool.prototype.showHorizontalMatches = function(part, guideline, snap) { var objBounds = part.locationObject.getDocumentBounds(); var p0 = objBounds.y; var p1 = objBounds.y + objBounds.height/2; var p2 = objBounds.y + objBounds.height; var marginOfError = this.guidelineSnapDistance; var distance = this.searchDistance; // compares with parts (or location objects) within narrow vertical area var area = objBounds.copy(); area.inflate(distance, marginOfError + 1); var otherParts = this.diagram.findObjectsIn(area, function(obj) { return obj.part; }, function(part) { return part instanceof go.Part && !part.isSelected && !(part instanceof go.Link) && part.isTopLevel && !part.layer.isTemporary; }, true); var bestDiff = marginOfError; var bestPart = null; var bestSpot; var bestOtherSpot; // horizontal line -- comparing y-values otherParts.each(function(other) { if (other === part) return; // ignore itself var otherBounds = other.locationObject.getDocumentBounds(); var q0 = otherBounds.y; var q1 = otherBounds.y + otherBounds.height/2; var q2 = otherBounds.y + otherBounds.height; // compare center with center of OTHER part if (Math.abs(p1 - q1) < bestDiff) { bestDiff = Math.abs(p1 - q1); bestPart = other; bestSpot = go.Spot.Center; bestOtherSpot = go.Spot.Center; } // compare top side with top and bottom sides of OTHER part if (Math.abs(p0-q0) < bestDiff) { bestDiff = Math.abs(p0-q0); bestPart = other; bestSpot = go.Spot.Top; bestOtherSpot = go.Spot.Top; } else if (Math.abs(p0-q2) < bestDiff) { bestDiff = Math.abs(p0-q2); bestPart = other; bestSpot = go.Spot.Top; bestOtherSpot = go.Spot.Bottom; } // compare bottom side with top and bottom sides of OTHER part if (Math.abs(p2-q0) < bestDiff) { bestDiff = Math.abs(p2-q0); bestPart = other; bestSpot = go.Spot.Bottom; bestOtherSpot = go.Spot.Top; } else if (Math.abs(p2-q2) < bestDiff) { bestDiff = Math.abs(p2-q2); bestPart = other; bestSpot = go.Spot.Bottom; bestOtherSpot = go.Spot.Bottom; } }); if (bestPart !== null) { var offsetX = objBounds.x - part.actualBounds.x; var offsetY = objBounds.y - part.actualBounds.y; var bestBounds = bestPart.locationObject.getDocumentBounds(); // line extends from x0 to x2 var x0 = Math.min(objBounds.x, bestBounds.x) - 10; var x2 = Math.max(objBounds.x + objBounds.width, bestBounds.x + bestBounds.width) + 10; // find bestPart's desired Y var bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot); if (bestSpot === go.Spot.Center) { if (snap) { // call Part.move in order to automatically move member Parts of Groups part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height / 2 - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineHcenter.position = new go.Point(x0, bestPoint.y); this.guidelineHcenter.elt(0).width = x2 - x0; this.diagram.add(this.guidelineHcenter); } } else if (bestSpot === go.Spot.Top) { if (snap) { part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineHtop.position = new go.Point(x0, bestPoint.y); this.guidelineHtop.elt(0).width = x2 - x0; this.diagram.add(this.guidelineHtop); } } else if (bestSpot === go.Spot.Bottom) { if (snap) { part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineHbottom.position = new go.Point(x0, bestPoint.y); this.guidelineHbottom.elt(0).width = x2 - x0; this.diagram.add(this.guidelineHbottom); } } } } /** * This finds parts that are aligned near the selected part along vertical lines. It compares the selected * part to all parts within a rectangle approximately twice the {@link #searchDistance} tall. * The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}. * The parameters used for {@link #guidelineSnap} are also set here. * @this {GuidedDraggingTool} * @param {Part} part * @param {boolean} guideline if true, show guideline * @param {boolean} snap if true, don't show guidelines but just snap the part to where the guideline would be */ GuidedDraggingTool.prototype.showVerticalMatches = function(part, guideline, snap) { var objBounds = part.locationObject.getDocumentBounds(); var p0 = objBounds.x; var p1 = objBounds.x + objBounds.width/2; var p2 = objBounds.x + objBounds.width; var marginOfError = this.guidelineSnapDistance; var distance = this.searchDistance; // compares with parts within narrow vertical area var area = objBounds.copy(); area.inflate(marginOfError + 1, distance); var otherParts = this.diagram.findObjectsIn(area, function(obj) { return obj.part; }, function(part) { return part instanceof go.Part && !part.isSelected && !(part instanceof go.Link) && part.isTopLevel && !part.layer.isTemporary; }, true); var bestDiff = marginOfError; var bestPart = null; var bestSpot; var bestOtherSpot; // vertical line -- comparing x-values otherParts.each(function(other) { if (other === part) return; // ignore itself var otherBounds = other.locationObject.getDocumentBounds(); var q0 = otherBounds.x; var q1 = otherBounds.x + otherBounds.width/2; var q2 = otherBounds.x + otherBounds.width; // compare center with center of OTHER part if (Math.abs(p1 - q1) < bestDiff) { bestDiff = Math.abs(p1 - q1); bestPart = other; bestSpot = go.Spot.Center; bestOtherSpot = go.Spot.Center; } // compare left side with left and right sides of OTHER part if (Math.abs(p0-q0) < bestDiff) { bestDiff = Math.abs(p0-q0); bestPart = other; bestSpot = go.Spot.Left; bestOtherSpot = go.Spot.Left; } else if (Math.abs(p0-q2) < bestDiff) { bestDiff = Math.abs(p0-q2); bestPart = other; bestSpot = go.Spot.Left; bestOtherSpot = go.Spot.Right; } // compare right side with left and right sides of OTHER part if (Math.abs(p2-q0) < bestDiff) { bestDiff = Math.abs(p2-q0); bestPart = other; bestSpot = go.Spot.Right; bestOtherSpot = go.Spot.Left; } else if (Math.abs(p2-q2) < bestDiff) { bestDiff = Math.abs(p2-q2); bestPart = other; bestSpot = go.Spot.Right; bestOtherSpot = go.Spot.Right; } }); if (bestPart !== null) { var offsetX = objBounds.x - part.actualBounds.x; var offsetY = objBounds.y - part.actualBounds.y; var bestBounds = bestPart.locationObject.getDocumentBounds(); // line extends from y0 to y2 var y0 = Math.min(objBounds.y, bestBounds.y) - 10; var y2 = Math.max(objBounds.y + objBounds.height, bestBounds.y + bestBounds.height) + 10; // find bestPart's desired X var bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot); if (bestSpot === go.Spot.Center) { if (snap) { // call Part.move in order to automatically move member Parts of Groups part.move(new go.Point(bestPoint.x - objBounds.width / 2 - offsetX, objBounds.y - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineVcenter.position = new go.Point(bestPoint.x, y0); this.guidelineVcenter.elt(0).height = y2 - y0; this.diagram.add(this.guidelineVcenter); } } else if (bestSpot === go.Spot.Left) { if (snap) { part.move(new go.Point(bestPoint.x - offsetX, objBounds.y - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineVleft.position = new go.Point(bestPoint.x, y0); this.guidelineVleft.elt(0).height = y2 - y0; this.diagram.add(this.guidelineVleft); } } else if (bestSpot === go.Spot.Right) { if (snap) { part.move(new go.Point(bestPoint.x - objBounds.width - offsetX, objBounds.y - offsetY)); this.invalidateLinks(part); } if (guideline) { this.guidelineVright.position = new go.Point(bestPoint.x, y0); this.guidelineVright.elt(0).height = y2 - y0; this.diagram.add(this.guidelineVright); } } } } /** * Gets or sets the margin of error for which guidelines show up. * The default value is 6. * Guidelines will show up when the aligned nods are ± 6px away from perfect alignment. * @name GuidedDraggingTool#guidelineSnapDistance * @function. * @return {number} */ Object.defineProperty(GuidedDraggingTool.prototype, "guidelineSnapDistance", { get: function() { return this._guidelineSnapDistance; }, set: function(val) { if (typeof val !== "number" || isNaN(val) || val < 0) throw new Error("new value for GuidedDraggingTool.guidelineSnapDistance must be a non-negative number."); if (this._guidelineSnapDistance !== val) { this._guidelineSnapDistance = val; } } }); /** * Gets or sets whether the guidelines are enabled or disable. * The default value is true. * @name GuidedDraggingTool#isGuidelineEnabled * @function. * @return {boolean} */ Object.defineProperty(GuidedDraggingTool.prototype, "isGuidelineEnabled", { get: function() { return this._isGuidelineEnabled; }, set: function(val) { if (typeof val !== "boolean") throw new Error("new value for GuidedDraggingTool.isGuidelineEnabled must be a boolean value."); if (this._isGuidelineEnabled !== val) { this._isGuidelineEnabled = val; } } }); /** * Gets or sets the color of horizontal guidelines. * The default value is "gray". * @name GuidedDraggingTool#horizontalGuidelineColor * @function. * @return {string} */ Object.defineProperty(GuidedDraggingTool.prototype, "horizontalGuidelineColor", { get: function() { return this._horizontalGuidelineColor; }, set: function(val) { if (this._horizontalGuidelineColor !== val) { this._horizontalGuidelineColor = val; this.guidelineHbottom.elements.first().stroke = this._horizontalGuidelineColor; this.guidelineHtop.elements.first().stroke = this._horizontalGuidelineColor; } } }); /** * Gets or sets the color of vertical guidelines. * The default value is "gray". * @name GuidedDraggingTool#verticalGuidelineColor * @function. * @return {string} */ Object.defineProperty(GuidedDraggingTool.prototype, "verticalGuidelineColor", { get: function() { return this._verticalGuidelineColor; }, set: function(val) { if (this._verticalGuidelineColor !== val) { this._verticalGuidelineColor = val; this.guidelineVleft.elements.first().stroke = this._verticalGuidelineColor; this.guidelineVright.elements.first().stroke = this._verticalGuidelineColor; } } }); /** * Gets or sets the color of center guidelines. * The default value is "gray". * @name GuidedDraggingTool#centerGuidelineColor * @function. * @return {string} */ Object.defineProperty(GuidedDraggingTool.prototype, "centerGuidelineColor", { get: function() { return this._centerGuidelineColor; }, set: function(val) { if (this._centerGuidelineColor !== val) { this._centerGuidelineColor = val; this.guidelineVcenter.elements.first().stroke = this._centerGuidelineColor; this.guidelineHcenter.elements.first().stroke = this._centerGuidelineColor; } } }); /** * Gets or sets the width guidelines. * The default value is 1. * @name GuidedDraggingTool#guidelineWidth * @function. * @return {number} */ Object.defineProperty(GuidedDraggingTool.prototype, "guidelineWidth", { get: function() { return this._guidelineWidth; }, set: function(val) { if (typeof val !== "number" || isNaN(val) || val < 0) throw new Error("New value for GuidedDraggingTool.guidelineWidth must be a non-negative number."); if (this._guidelineWidth !== val) { this._guidelineWidth = val; this.guidelineVcenter.elements.first().strokeWidth = val; this.guidelineHcenter.elements.first().strokeWidth = val; this.guidelineVleft.elements.first().strokeWidth = val; this.guidelineVright.elements.first().strokeWidth = val; this.guidelineHbottom.elements.first().strokeWidth = val; this.guidelineHtop.elements.first().strokeWidth = val; } } }); /** * Gets or sets the distance around the selected part to search for aligned parts. * The default value is 1000. * Set this to Infinity if you want to search the entire diagram no matter how far away. * @name GuidedDraggingTool#searchDistance * @function. * @return {number} */ Object.defineProperty(GuidedDraggingTool.prototype, "searchDistance", { get: function() { return this._searchDistance; }, set: function(val) { if (typeof val !== "number" || isNaN(val) || val <= 0) throw new Error("new value for GuidedDraggingTool.searchDistance must be a positive number."); if (this._searchDistance !== val) { this._searchDistance = val; } } }); /** * Gets or sets whether snapping to guidelines is enabled. * The default value is true. * @name GuidedDraggingTool#isGuidelineSnapEnabled * @function. * @return {Boolean} */ Object.defineProperty(GuidedDraggingTool.prototype, "isGuidelineSnapEnabled", { get: function() { return this._isGuidelineSnapEnabled; }, set: function(val) { if (typeof val !== "boolean") throw new Error("new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean."); if (this._isGuidelineSnapEnabled !== val) { this._isGuidelineSnapEnabled = val; } } });