1 import {Workflow} from "../..";
2 import {PluginBase} from "../plugin-base";
3 import {EdgePanner} from "../../behaviors/edge-panning";
5 export interface ConstructorParams {
6 movementSpeed?: number,
11 * This plugin makes node dragging and movement possible.
13 * @FIXME: attach events for before and after change
15 export class SVGNodeMovePlugin extends PluginBase {
17 /** Difference in movement on the X axis since drag start, adapted for scale and possibly panned distance */
20 /** Difference in movement on the Y axis since drag start, adapted for scale and possibly panned distance */
23 /** Stored onDragStart so we can put node to a fixed position determined by startX + ∆x */
24 private startX?: number;
26 /** Stored onDragStart so we can put node to a fixed position determined by startY + ∆y */
27 private startY?: number;
29 /** How far from the edge of the viewport does mouse need to be before panning is triggered */
30 private scrollMargin = 50;
32 /** How fast does workflow move while panning */
33 private movementSpeed = 10;
35 /** Holds an element that is currently being dragged. Stored onDragStart and translated afterwards. */
36 private movingNode?: SVGGElement;
38 /** Stored onDragStart to detect collision with viewport edges */
39 private boundingClientRect?: ClientRect;
41 /** Cache input edges and their parsed bezier curve parameters so we don't query for them on each mouse move */
42 private inputEdges?: Map<SVGPathElement, number[]>;
44 /** Cache output edges and their parsed bezier curve parameters so we don't query for them on each mouse move */
45 private outputEdges?: Map<SVGPathElement, number[]>;
47 /** Workflow panning at the time of onDragStart, used to adjust ∆x and ∆y while panning */
48 private startWorkflowTranslation?: { x: number, y: number };
50 private wheelPrevent = (ev: any) => ev.stopPropagation();
52 private boundMoveHandler = this.onMove.bind(this);
53 private boundMoveStartHandler = this.onMoveStart.bind(this);
54 private boundMoveEndHandler = this.onMoveEnd.bind(this);
56 private detachDragListenerFn: any = undefined;
58 private edgePanner: EdgePanner;
60 constructor(parameters: ConstructorParams = {}) {
62 Object.assign(this, parameters);
66 onEditableStateChange(enabled: boolean): void {
77 if (this.workflow.editingEnabled) {
87 registerWorkflow(workflow: Workflow): void {
88 super.registerWorkflow(workflow);
90 this.edgePanner = new EdgePanner(this.workflow, {
91 scrollMargin: this.scrollMargin,
92 movementSpeed: this.movementSpeed
96 private detachDrag() {
97 if (typeof this.detachDragListenerFn === "function") {
98 this.detachDragListenerFn();
101 this.detachDragListenerFn = undefined;
104 private attachDrag() {
108 this.detachDragListenerFn = this.workflow.domEvents.drag(
110 this.boundMoveHandler,
111 this.boundMoveStartHandler,
112 this.boundMoveEndHandler
116 private getWorkflowMatrix(): SVGMatrix {
117 return this.workflow.workflow.transform.baseVal.getItem(0).matrix;
120 private onMove(dx: number, dy: number, ev: MouseEvent): void {
122 /** We will use workflow scale to determine how our mouse movement translate to svg proportions */
123 const scale = this.workflow.scale;
125 /** Need to know how far did the workflow itself move since when we started dragging */
126 const matrixMovement = {
127 x: this.getWorkflowMatrix().e - this.startWorkflowTranslation!.x,
128 y: this.getWorkflowMatrix().f - this.startWorkflowTranslation!.y
131 /** We might have hit the boundary and need to start panning */
132 this.edgePanner.triggerCollisionDetection(ev.clientX, ev.clientY, (sdx, sdy) => {
136 this.translateNodeBy(this.movingNode!, sdx, sdy);
137 this.redrawEdges(this.sdx, this.sdy);
141 * We need to store scaled ∆x and ∆y because this is not the only place from which node is being moved.
142 * If mouse is outside the viewport, and the workflow is panning, startScroll will continue moving
143 * this node, so it needs to know where to start from and update it, so this method can take
144 * over when mouse gets back to the viewport.
146 * If there was no handoff, node would jump back and forth to
147 * last positions for each movement initiator separately.
149 this.sdx = (dx - matrixMovement.x) / scale;
150 this.sdy = (dy - matrixMovement.y) / scale;
152 const moveX = this.sdx + this.startX!;
153 const moveY = this.sdy + this.startY!;
155 this.translateNodeTo(this.movingNode!, moveX, moveY);
156 this.redrawEdges(this.sdx, this.sdy);
160 * Triggered from {@link attachDrag} when drag starts.
161 * This method initializes properties that are needed for calculations during movement.
163 private onMoveStart(event: MouseEvent, handle: SVGGElement): void {
165 /** We will query the SVG dom for edges that we need to move, so store svg element for easy access */
166 const svg = this.workflow.svgRoot;
168 document.addEventListener("mousewheel", this.wheelPrevent, true);
170 /** Our drag handle is not the whole node because that would include ports and labels, but a child of it*/
171 const node = handle.parentNode as SVGGElement;
173 /** Store initial transform values so we know how much we've moved relative from the starting position */
174 const nodeMatrix = node.transform.baseVal.getItem(0).matrix;
175 this.startX = nodeMatrix.e;
176 this.startY = nodeMatrix.f;
178 /** We have to query for edges that are attached to this node because we will move them as well */
179 const nodeID = node.getAttribute("data-id");
182 * When user drags the node to the edge and waits while workflow pans to the side,
183 * mouse movement stops, but workflow movement starts.
184 * We then utilize this to get movement ∆ of the workflow, and use that for translation instead.
186 this.startWorkflowTranslation = {
187 x: this.getWorkflowMatrix().e,
188 y: this.getWorkflowMatrix().f
191 /** Used to determine whether dragged node is hitting the edge, so we can pan the Workflow*/
192 this.boundingClientRect = svg.getBoundingClientRect();
194 /** Node movement can be initiated from both mouse events and animationFrame, so make it accessible */
195 this.movingNode = handle.parentNode as SVGGElement;
198 * While node is being moved, incoming and outgoing edges also need to be moved in order to stay attached.
199 * We don't want to query them all the time, so we cache them in maps that point from their dom elements
200 * to an array of numbers that represent their bezier curves, since we will update those curves.
202 this.inputEdges = new Map();
203 this.outputEdges = new Map();
205 const outputsSelector = `.edge[data-source-node='${nodeID}'] .sub-edge`;
206 const inputsSelector = `.edge[data-destination-node='${nodeID}'] .sub-edge`;
208 const query: any = svg.querySelectorAll([inputsSelector, outputsSelector].join(", ")) as NodeListOf<SVGPathElement>;
210 for (let subEdge of query) {
211 const isInput = subEdge.parentElement.getAttribute("data-destination-node") === nodeID;
212 const path = subEdge.getAttribute("d").split(" ").map(Number).filter((e: any) => !isNaN(e));
213 isInput ? this.inputEdges.set(subEdge, path) : this.outputEdges.set(subEdge, path);
217 private translateNodeBy(node: SVGGElement, x?: number, y?: number): void {
218 const matrix = node.transform.baseVal.getItem(0).matrix;
219 this.translateNodeTo(node, matrix.e + x!, matrix.f + y!);
222 private translateNodeTo(node: SVGGElement, x?: number, y?: number): void {
223 node.transform.baseVal.getItem(0).setTranslate(x!, y!);
227 * Redraws stored input and output edges so as to transform them with respect to
228 * scaled transformation differences, sdx and sdy.
230 private redrawEdges(sdx: number, sdy: number): void {
231 this.inputEdges!.forEach((p, el) => {
232 const path = Workflow.makeConnectionPath(p[0], p[1], p[6] + sdx, p[7] + sdy);
233 el.setAttribute("d", path!);
236 this.outputEdges!.forEach((p, el) => {
237 const path = Workflow.makeConnectionPath(p[0] + sdx, p[1] + sdy, p[6], p[7]);
238 el.setAttribute("d", path!);
243 * Triggered from {@link attachDrag} after move event ends
245 private onMoveEnd(): void {
247 this.edgePanner.stop();
249 const id = this.movingNode!.getAttribute("data-connection-id")!;
250 const nodeModel = this.workflow.model.findById(id);
252 if (!nodeModel.customProps) {
253 nodeModel.customProps = {};
256 const matrix = this.movingNode!.transform.baseVal.getItem(0).matrix;
258 Object.assign(nodeModel.customProps, {
263 this.onAfterChange({type: "node-move"});
265 document.removeEventListener("mousewheel", this.wheelPrevent, true);
269 delete this.movingNode;
270 delete this.inputEdges;
271 delete this.outputEdges;
272 delete this.boundingClientRect;
273 delete this.startWorkflowTranslation;