--- /dev/null
+import {Workflow} from "../..";
+import {PluginBase} from "../plugin-base";
+import {EdgePanner} from "../../behaviors/edge-panning";
+
+export interface ConstructorParams {
+ movementSpeed?: number,
+ scrollMargin?: number
+}
+
+/**
+ * This plugin makes node dragging and movement possible.
+ *
+ * @FIXME: attach events for before and after change
+ */
+export class SVGNodeMovePlugin extends PluginBase {
+
+ /** Difference in movement on the X axis since drag start, adapted for scale and possibly panned distance */
+ private sdx: number;
+
+ /** Difference in movement on the Y axis since drag start, adapted for scale and possibly panned distance */
+ private sdy: number;
+
+ /** Stored onDragStart so we can put node to a fixed position determined by startX + ∆x */
+ private startX: number;
+
+ /** Stored onDragStart so we can put node to a fixed position determined by startY + ∆y */
+ private startY: number;
+
+ /** How far from the edge of the viewport does mouse need to be before panning is triggered */
+ private scrollMargin = 50;
+
+ /** How fast does workflow move while panning */
+ private movementSpeed = 10;
+
+ /** Holds an element that is currently being dragged. Stored onDragStart and translated afterwards. */
+ private movingNode: SVGGElement;
+
+ /** Stored onDragStart to detect collision with viewport edges */
+ private boundingClientRect: ClientRect;
+
+ /** Cache input edges and their parsed bezier curve parameters so we don't query for them on each mouse move */
+ private inputEdges: Map<SVGPathElement, number[]>;
+
+ /** Cache output edges and their parsed bezier curve parameters so we don't query for them on each mouse move */
+ private outputEdges: Map<SVGPathElement, number[]>;
+
+ /** Workflow panning at the time of onDragStart, used to adjust ∆x and ∆y while panning */
+ private startWorkflowTranslation: { x: number, y: number };
+
+ private wheelPrevent = (ev: any) => ev.stopPropagation();
+
+ private boundMoveHandler = this.onMove.bind(this);
+ private boundMoveStartHandler = this.onMoveStart.bind(this);
+ private boundMoveEndHandler = this.onMoveEnd.bind(this);
+
+ private detachDragListenerFn: any = undefined;
+
+ private edgePanner: EdgePanner;
+
+ constructor(parameters: ConstructorParams = {}) {
+ super();
+ Object.assign(this, parameters);
+ }
+
+
+ onEditableStateChange(enabled: boolean): void {
+
+ if (enabled) {
+ this.attachDrag();
+ } else {
+ this.detachDrag();
+ }
+ }
+
+ afterRender() {
+
+ if (this.workflow.editingEnabled) {
+ this.attachDrag();
+ }
+
+ }
+
+ destroy(): void {
+ this.detachDrag();
+ }
+
+ registerWorkflow(workflow: Workflow): void {
+ super.registerWorkflow(workflow);
+
+ this.edgePanner = new EdgePanner(this.workflow, {
+ scrollMargin: this.scrollMargin,
+ movementSpeed: this.movementSpeed
+ });
+ }
+
+ private detachDrag() {
+ if (typeof this.detachDragListenerFn === "function") {
+ this.detachDragListenerFn();
+ }
+
+ this.detachDragListenerFn = undefined;
+ }
+
+ private attachDrag() {
+
+ this.detachDrag();
+
+ this.detachDragListenerFn = this.workflow.domEvents.drag(
+ ".node .core",
+ this.boundMoveHandler,
+ this.boundMoveStartHandler,
+ this.boundMoveEndHandler
+ );
+ }
+
+ private getWorkflowMatrix(): SVGMatrix {
+ return this.workflow.workflow.transform.baseVal.getItem(0).matrix;
+ }
+
+ private onMove(dx: number, dy: number, ev: MouseEvent): void {
+
+ /** We will use workflow scale to determine how our mouse movement translate to svg proportions */
+ const scale = this.workflow.scale;
+
+ /** Need to know how far did the workflow itself move since when we started dragging */
+ const matrixMovement = {
+ x: this.getWorkflowMatrix().e - this.startWorkflowTranslation.x,
+ y: this.getWorkflowMatrix().f - this.startWorkflowTranslation.y
+ };
+
+ /** We might have hit the boundary and need to start panning */
+ this.edgePanner.triggerCollisionDetection(ev.clientX, ev.clientY, (sdx, sdy) => {
+ this.sdx += sdx;
+ this.sdy += sdy;
+
+ this.translateNodeBy(this.movingNode, sdx, sdy);
+ this.redrawEdges(this.sdx, this.sdy);
+ });
+
+ /**
+ * We need to store scaled ∆x and ∆y because this is not the only place from which node is being moved.
+ * If mouse is outside the viewport, and the workflow is panning, startScroll will continue moving
+ * this node, so it needs to know where to start from and update it, so this method can take
+ * over when mouse gets back to the viewport.
+ *
+ * If there was no handoff, node would jump back and forth to
+ * last positions for each movement initiator separately.
+ */
+ this.sdx = (dx - matrixMovement.x) / scale;
+ this.sdy = (dy - matrixMovement.y) / scale;
+
+ const moveX = this.sdx + this.startX;
+ const moveY = this.sdy + this.startY;
+
+ this.translateNodeTo(this.movingNode, moveX, moveY);
+ this.redrawEdges(this.sdx, this.sdy);
+ }
+
+ /**
+ * Triggered from {@link attachDrag} when drag starts.
+ * This method initializes properties that are needed for calculations during movement.
+ */
+ private onMoveStart(event: MouseEvent, handle: SVGGElement): void {
+
+ /** We will query the SVG dom for edges that we need to move, so store svg element for easy access */
+ const svg = this.workflow.svgRoot;
+
+ document.addEventListener("mousewheel", this.wheelPrevent, true);
+
+ /** Our drag handle is not the whole node because that would include ports and labels, but a child of it*/
+ const node = handle.parentNode as SVGGElement;
+
+ /** Store initial transform values so we know how much we've moved relative from the starting position */
+ const nodeMatrix = node.transform.baseVal.getItem(0).matrix;
+ this.startX = nodeMatrix.e;
+ this.startY = nodeMatrix.f;
+
+ /** We have to query for edges that are attached to this node because we will move them as well */
+ const nodeID = node.getAttribute("data-id");
+
+ /**
+ * When user drags the node to the edge and waits while workflow pans to the side,
+ * mouse movement stops, but workflow movement starts.
+ * We then utilize this to get movement ∆ of the workflow, and use that for translation instead.
+ */
+ this.startWorkflowTranslation = {
+ x: this.getWorkflowMatrix().e,
+ y: this.getWorkflowMatrix().f
+ };
+
+ /** Used to determine whether dragged node is hitting the edge, so we can pan the Workflow*/
+ this.boundingClientRect = svg.getBoundingClientRect();
+
+ /** Node movement can be initiated from both mouse events and animationFrame, so make it accessible */
+ this.movingNode = handle.parentNode as SVGGElement;
+
+ /**
+ * While node is being moved, incoming and outgoing edges also need to be moved in order to stay attached.
+ * We don't want to query them all the time, so we cache them in maps that point from their dom elements
+ * to an array of numbers that represent their bezier curves, since we will update those curves.
+ */
+ this.inputEdges = new Map();
+ this.outputEdges = new Map();
+
+ const outputsSelector = `.edge[data-source-node='${nodeID}'] .sub-edge`;
+ const inputsSelector = `.edge[data-destination-node='${nodeID}'] .sub-edge`;
+
+ const query: any = svg.querySelectorAll([inputsSelector, outputsSelector].join(", ")) as NodeListOf<SVGPathElement>;
+
+ for (let subEdge of query) {
+ const isInput = subEdge.parentElement.getAttribute("data-destination-node") === nodeID;
+ const path = subEdge.getAttribute("d").split(" ").map(Number).filter((e: any) => !isNaN(e));
+ isInput ? this.inputEdges.set(subEdge, path) : this.outputEdges.set(subEdge, path);
+ }
+ }
+
+ private translateNodeBy(node: SVGGElement, x?: number, y?: number): void {
+ const matrix = node.transform.baseVal.getItem(0).matrix;
+ this.translateNodeTo(node, matrix.e + x!, matrix.f + y!);
+ }
+
+ private translateNodeTo(node: SVGGElement, x?: number, y?: number): void {
+ node.transform.baseVal.getItem(0).setTranslate(x!, y!);
+ }
+
+ /**
+ * Redraws stored input and output edges so as to transform them with respect to
+ * scaled transformation differences, sdx and sdy.
+ */
+ private redrawEdges(sdx: number, sdy: number): void {
+ this.inputEdges.forEach((p, el) => {
+ const path = Workflow.makeConnectionPath(p[0], p[1], p[6] + sdx, p[7] + sdy);
+ el.setAttribute("d", path!);
+ });
+
+ this.outputEdges.forEach((p, el) => {
+ const path = Workflow.makeConnectionPath(p[0] + sdx, p[1] + sdy, p[6], p[7]);
+ el.setAttribute("d", path!);
+ });
+ }
+
+ /**
+ * Triggered from {@link attachDrag} after move event ends
+ */
+ private onMoveEnd(): void {
+
+ this.edgePanner.stop();
+
+ const id = this.movingNode.getAttribute("data-connection-id")!;
+ const nodeModel = this.workflow.model.findById(id);
+
+ if (!nodeModel.customProps) {
+ nodeModel.customProps = {};
+ }
+
+ const matrix = this.movingNode.transform.baseVal.getItem(0).matrix;
+
+ Object.assign(nodeModel.customProps, {
+ "sbg:x": matrix.e,
+ "sbg:y": matrix.f,
+ });
+
+ this.onAfterChange({type: "node-move"});
+
+ document.removeEventListener("mousewheel", this.wheelPrevent, true);
+
+ delete this.startX;
+ delete this.startY;
+ delete this.movingNode;
+ delete this.inputEdges;
+ delete this.outputEdges;
+ delete this.boundingClientRect;
+ delete this.startWorkflowTranslation;
+ }
+
+
+}