diff --git a/demos/demo13/index.tsx b/demos/demo13/index.tsx new file mode 100644 index 0000000..8497ad5 --- /dev/null +++ b/demos/demo13/index.tsx @@ -0,0 +1,114 @@ +import { + DiagramEngine, + DefaultNodeFactory, + DefaultLinkFactory, + DiagramModel, + DefaultNodeModel, + LinkModel, + NodeModel, + DefaultPortModel, + DiagramWidget, + DefaultNodeInstanceFactory, + DefaultPortInstanceFactory, + LinkInstanceFactory +} from "../../src/main"; +import * as React from "react"; + +/** + * Tests the grid size + */ +class NodeDelayedPosition extends React.Component { + constructor(props) { + super(props); + this.cloneSelected = this.cloneSelected.bind(this) + } + + updatePosition() { + const { engine } = this.props; + let model = engine.getDiagramModel(); + const nodes = model.getNodes(); + let node = nodes[Object.keys(nodes)[0]]; + node.setPosition(node.x + 30, node.y + 30); + this.forceUpdate(); + } + + cloneSelected() { + const {engine} = this.props; + const offset = {x:100,y:100}; + const model = engine.getDiagramModel(); + const originalItems = model.getSelectedItems(); + const selectedItems = originalItems.reduce((res,i) => { + if(i instanceof NodeModel) res.nodes.push(i); + else if(i instanceof LinkModel) res.links.push(i); + return res; + },{nodes:[],links:[]}); + let lookupTable = {}; + selectedItems.nodes.forEach(i => { + + let node = i.clone(lookupTable); + model.addNode(node); + node.setPosition(node.x+offset.x,node.y+offset.y) + }); + selectedItems.links.forEach(i => { + let link = i.clone(lookupTable); + link.getPoints().forEach(p => p.updateLocation({x:p.getX()+offset.x,y:p.getY()+offset.y})); + model.addLink(link); + }); + originalItems.forEach(i => i.selected = false); + this.forceUpdate(); + } + + + render() { + const { engine } = this.props; + return ( +
+ + +
+ ); + } +} + +export default () => { + //1) setup the diagram engine + var engine = new DiagramEngine(); + engine.registerNodeFactory(new DefaultNodeFactory()); + engine.registerLinkFactory(new DefaultLinkFactory()); + + //2) setup the diagram model + var model = new DiagramModel(); + + //3-A) create a default node + var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)"); + var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out")); + node1.x = 100; + node1.y = 100; + + //3-B) create another default node + var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)"); + var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN")); + node2.x = 400; + node2.y = 100; + + //3-C) link the 2 nodes together + var link1 = new LinkModel(); + link1.setSourcePort(port1); + link1.setTargetPort(port2); + + //4) add the models to the root graph + model.addNode(node1); + model.addNode(node2); + model.addLink(link1); + + //5) load model into engine + engine.setDiagramModel(model); + + //we need this to help the system know what models to create form the JSON + engine.registerInstanceFactory(new DefaultNodeInstanceFactory()); + engine.registerInstanceFactory(new DefaultPortInstanceFactory()); + engine.registerInstanceFactory(new LinkInstanceFactory()); + + //6) render the diagram! + return ; +}; diff --git a/demos/index.tsx b/demos/index.tsx index c15cf08..e01c58c 100644 --- a/demos/index.tsx +++ b/demos/index.tsx @@ -15,6 +15,7 @@ import demo9 from "./demo9/index"; import demo10 from "./demo10/index"; import demo11 from "./demo11/index"; import demo12 from "./demo12/index"; +import demo13 from "./demo13/index"; import demoDagre from "./demo-dagre/index"; import { Helper } from "./Helper"; @@ -62,6 +63,9 @@ storiesOf("React Diagrams", module) }) .add("Link types", () => { return demo12(); + }) + .add("Clone selected", () => { + return demo13(); }); // enable this to log mouse location when writing new puppeteer tests diff --git a/src/BaseEntity.ts b/src/BaseEntity.ts index 2980706..125186e 100644 --- a/src/BaseEntity.ts +++ b/src/BaseEntity.ts @@ -29,13 +29,10 @@ export class BaseEntity { return this.id; } - clone() { - var clone=_.clone(this); - clone.id = Toolkit.UID(); + clone(lookupTable) { + let clone = _.clone(this); clone.clearListeners(); - this.iterateListeners(l => { - clone.addListener(_.clone(l)); - }) + clone.id = Toolkit.UID(); return clone; } diff --git a/src/models/BaseModel.ts b/src/models/BaseModel.ts index 906e96d..7167412 100644 --- a/src/models/BaseModel.ts +++ b/src/models/BaseModel.ts @@ -40,6 +40,14 @@ export class BaseModel extends }); } + public clone(lookupTable) { + if(((lookupTable||{})[this.class]||{}).hasOwnProperty(this.id)) return lookupTable[this.class][this.id]; + let clone = super.clone(lookupTable); + if(!lookupTable[this.class]) lookupTable[this.class] = {}; + lookupTable[this.class][this.id] = clone; + return clone; + } + public getID(): string { return this.id; } diff --git a/src/models/LinkModel.ts b/src/models/LinkModel.ts index c0a251a..4319ce0 100644 --- a/src/models/LinkModel.ts +++ b/src/models/LinkModel.ts @@ -51,6 +51,19 @@ export class LinkModel extends BaseModel { }); } + clone(lookupTable) { + if(((lookupTable||{})[this.class]||{}).hasOwnProperty(this.id)) return lookupTable[this.class][this.id]; + let clone = super.clone(lookupTable); + clone.setPoints(_.map(clone.getPoints(),(point:PointModel) => { + let newPoint = point.clone(lookupTable); + newPoint.link = clone; + return newPoint; + })); + if(this.sourcePort) clone.setSourcePort(this.sourcePort.clone(lookupTable)); + if(this.targetPort) clone.setTargetPort(this.targetPort.clone(lookupTable)); + return clone + } + remove() { if (this.sourcePort) { this.sourcePort.removeLink(this); @@ -143,6 +156,15 @@ export class LinkModel extends BaseModel { this.points.splice(this.getPointIndex(pointModel), 1); } + removePointsBefore(pointModel: PointModel) { + this.points.splice(0,this.getPointIndex(pointModel)) + } + + removePointsAfter(pointModel: PointModel) { + + this.points.splice(this.getPointIndex(pointModel)+1) + } + addPoint(pointModel: PointModel, index = 1) { this.points.splice(index, 0, pointModel); } diff --git a/src/models/NodeModel.ts b/src/models/NodeModel.ts index cd78060..02297af 100644 --- a/src/models/NodeModel.ts +++ b/src/models/NodeModel.ts @@ -22,7 +22,6 @@ export class NodeModel extends BaseModel { //store position let oldX = this.x; let oldY = this.y; - for (let port in this.ports) { _.forEach(this.ports[port].getLinks(), link => { let point = link.getPointForPort(this.ports[port]); @@ -71,6 +70,16 @@ export class NodeModel extends BaseModel { }); } + clone(lookupTable) { + if(((lookupTable||{})[this.class]||{}).hasOwnProperty(this.id)) return lookupTable[this.class][this.id]; + let clone = super.clone(lookupTable); + clone.ports = {} + _.values(this.ports).map(port => { + clone.addPort(port.clone(lookupTable)); + }) + return clone + } + remove() { super.remove(); for (var i in this.ports) { diff --git a/src/models/PortModel.ts b/src/models/PortModel.ts index 8202615..72f63ab 100644 --- a/src/models/PortModel.ts +++ b/src/models/PortModel.ts @@ -23,6 +23,15 @@ export class PortModel extends BaseModel { }); } + clone(lookupTable) { + if(((lookupTable||{})[this.class]||{}).hasOwnProperty(this.id)) return lookupTable[this.class][this.id]; + let clone = super.clone(lookupTable); + //we are merely a referenced object. The links/nodes should be in charge of handling our connections + clone.links = {}; + clone.parentNode = null; + return clone + } + constructor(name: string, id?: string) { super(id); this.name = name; diff --git a/src/widgets/DiagramWidget.tsx b/src/widgets/DiagramWidget.tsx index 5a815bf..25a9c44 100644 --- a/src/widgets/DiagramWidget.tsx +++ b/src/widgets/DiagramWidget.tsx @@ -302,35 +302,29 @@ export class DiagramWidget extends React.Component { if (element && element.model instanceof PortModel && !diagramEngine.isModelLocked(element.model)) { linkConnected = true; let link = model.model.getLink(); - //if this was a valid link already and we are adding a node in the middle, create 2 links from one - if(link.getTargetPort() !== undefined) + if(link.getTargetPort() !== null) { - var newLink = link.clone(); - newLink.setSourcePort(element.model); - newLink.setTargetPort(link.getTargetPort()); - var prePoints = [] - var postPoints = [] - var found = false - _.forEach(link.getPoints(), point => { - if(point.id === model.model.id) - { - found = true; - prePoints.push(point); - postPoints.push(point); - } else { - if(found) - { - prePoints.push(point); - } else { - postPoints.push(point); - } - } - }) - link.setPoints(prePoints); - newLink.setPoints(postPoints); - diagramEngine.getDiagramModel().addLink(newLink) + //if this was a valid link already and we are adding a node in the middle, create 2 links from the original + if(link.getTargetPort() !== element.model && link.getSourcePort() !== element.model) + { + const targetPort = link.getTargetPort(); + let newLink = link.clone({}); + newLink.setSourcePort(element.model); + newLink.setTargetPort(targetPort); + link.setTargetPort(element.model); + targetPort.removeLink(link); + newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]); + link.removePointsAfter(model.model); + diagramEngine.getDiagramModel().addLink(newLink) + //if we are connecting to the same target or source, remove tweener points + } else if(link.getTargetPort() === element.model) { + link.removePointsAfter(model.model); + } else if(link.getSourcePort() === element.model){ + link.removePointsBefore(model.model); + } + } else { + link.setTargetPort(element.model); } - link.setTargetPort(element.model); delete this.props.diagramEngine.linksThatHaveInitiallyRendered[link.getID()]; } //if we moved a NodeModel and allowLooseLinks is false, we know that any links involved were valid @@ -360,7 +354,6 @@ export class DiagramWidget extends React.Component { diagramEngine.clearRepaintEntities(); this.stopFiringAction(); } - this.state.document.removeEventListener("mousemove", this.onMouseMove); this.state.document.removeEventListener("mouseup", this.onMouseUp); } diff --git a/tests/__snapshots__/2.test.tsx.snap b/tests/__snapshots__/2.test.tsx.snap index e258954..311f44e 100644 --- a/tests/__snapshots__/2.test.tsx.snap +++ b/tests/__snapshots__/2.test.tsx.snap @@ -470,6 +470,154 @@ exports[`Storyshots React Diagrams Auto distribute 1`] = ` `; +exports[`Storyshots React Diagrams Clone selected 1`] = ` +
+
+ +
+
+
+
+
+ Node 1 +
+
+
+
+
+
+
+ Out +
+
+
+
+
+
+
+
+
+
+
+ Node 2 +
+
+
+
+
+
+
+ IN +
+
+
+
+
+
+
+
+
+ +
+`; + exports[`Storyshots React Diagrams Custom Diamond Widget 1`] = `