housekeeping
83
README.md
@ -6,11 +6,15 @@ A super simple, no-nonsense diagramming library written in React that just works
|
||||
[](https://npmjs.org/package/storm-react-diagrams)
|
||||
[](https://npmjs.org/package/storm-react-diagrams)
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
A no-nonsense diagramming library written entirely in React with the help of Lodash. It aims to be:
|
||||
A no-nonsense diagramming library written entirely in React with the help of Lodash, and a single polyfill. It aims to be:
|
||||
|
||||
* Simple, and void of any fuss/complications when implementing it into your own application
|
||||
* Customizable without having to hack the core (adapters/factories etc..)
|
||||
@ -28,22 +32,17 @@ or
|
||||
yarn add storm-react-diagrams
|
||||
```
|
||||
|
||||
* Its only dependency is Lodash and obviously React so it will install that too.
|
||||
## How to build
|
||||
|
||||
#### How to build
|
||||
Simply run ```webpack``` in the root directory (or ```export NODE_ENV=production && webpack``` if you want a production build) and it will spit out the transpiled code and typescript definitions into the dist directory as a single file. __It will also compile the code for the demos__ .We use webpack for this because TSC cannot compile a single UMD file (TSC can currently only output multiple UMD files).
|
||||
|
||||
Simply run ```webpack``` in the root directory (or ```webpack -p``` if you want a production build) and it will spit out the transpiled code and typescript definitions into the dist directory as a single file. __It will also compile the code for the demos__ .We use webpack for this because TSC cannot compile a single UMD file (TSC can currently only output multiple UMD files).
|
||||
_NOTE:_ We turn off name mangeling in production builds because we require class names to be left intact when serializing.
|
||||
|
||||
#### How to see the examples
|
||||
|
||||
1. checkout the project
|
||||
2. run ```webpack``` in the root
|
||||
3. open up one of the __demos__ folders and load the corresponding index.html file.
|
||||
## Make your own nodes
|
||||
|
||||
To see how to create your own nodes like the one below, take a look at __demo3__:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## How does it work
|
||||
|
||||
@ -62,57 +61,29 @@ a link can be connected to it.
|
||||
|
||||
[Event System](docs/Events.md)
|
||||
|
||||
## DiagramWidget props
|
||||
|
||||
- onLinkStateChanged (link, isConnected)
|
||||
- diagramEngine
|
||||
|
||||
|
||||
## Questions
|
||||
|
||||
#### Why didn’t I render the nodes as SVG's?
|
||||
[Questions](docs/Questions.md)
|
||||
|
||||
Because its vastly better to render nodes as standard HTML so that we can embed input controls and not have
|
||||
to deal with the complexities of trying to get SVG to work like we want it to. I also created this primarily to embed into
|
||||
enterprise applications where the nodes themselves are highly interactive with buttons and other controls that cave when I try to use SVG.
|
||||
## Usage
|
||||
|
||||
#### Why Typescript?
|
||||
__Delete__ removes any selected items
|
||||

|
||||
|
||||
Because it can transpile into any level of ECMA Script, and the library got really complicated, so I ported it to Typescript
|
||||
to accommodate the heavy architectural changes I was starting to make. <3 Type Script
|
||||
__Shift + Mouse Drag__ triggers a multi-selection box
|
||||

|
||||
|
||||
#### Why is there no JSX?
|
||||
__Shift + Mouse Click__ selects the item (items can be multi-selected)
|
||||

|
||||
|
||||
Because most of the library is 95% all logic anyway, and I construct very complex DOM elements with many dynamic properties. JSX
|
||||
Would just get in the way, and I personally hate JSX for a multitude of reasons anyway.
|
||||
__Mouse Drag__ drags the entire diagram
|
||||

|
||||
|
||||
#### How do I make my own elements?
|
||||
__Mouse Wheel__ zooms the diagram in / out
|
||||

|
||||
|
||||
Take a look at the __defaults__ directory, with specific attention to the __DefaultNodeWidget__
|
||||
__Click Link + Drag__ creates a new link point
|
||||

|
||||
|
||||
#### How do I use the library?
|
||||
|
||||
Take a look at the demo folders, they have simple and complex examples of the complete usage.
|
||||
|
||||
## Usage Demo and Guide
|
||||
|
||||
This is a demo of the interaction taken directly from the test folder.
|
||||
|
||||

|
||||
|
||||
#### Key commands
|
||||
|
||||
__del key__ will remove anything selected including links
|
||||
|
||||
__shift and drag__ will trigger a multi selection box
|
||||
|
||||
__shift and select nodes/links/points__ will select multiple nodes
|
||||
|
||||
__drag canvas__ will drag the complete diagram
|
||||
|
||||
__mouse wheel__ will zoom in or out the entire diagram
|
||||
|
||||
__click link and drag__ will create a new link anchor/point that you can then drag around
|
||||
|
||||
__click node-port and drag__ will create a new link that is anchored to the port, allowing you
|
||||
to drag the link to another connecting port
|
||||
__Click Node Port + Drag__ creates a new link
|
||||

|
||||
|
BIN
custom-nodes.png
Before Width: | Height: | Size: 34 KiB |
@ -16,7 +16,7 @@ export interface DiamonNodeWidgetState {
|
||||
export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, DiamonNodeWidgetState> {
|
||||
|
||||
public static defaultProps: DiamonNodeWidgetProps = {
|
||||
size:150,
|
||||
size: 150,
|
||||
node: null
|
||||
};
|
||||
|
||||
@ -34,27 +34,27 @@ export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, Dia
|
||||
<g id="Layer_1">
|
||||
</g>
|
||||
<g id="Layer_2">
|
||||
<polygon fill="cyan" stroke="#000000" stroke-width="3" stroke-miterlimit="10" points="10,`+(this.props.size/2)+` `+(this.props.size/2)+`,10 `+(this.props.size-10)+`,`+(this.props.size/2)+` `+(this.props.size/2)+`,`+(this.props.size-10)+` "/>
|
||||
<polygon fill="purple" stroke="#000000" stroke-width="3" stroke-miterlimit="10" points="10,`+(this.props.size/2)+` `+(this.props.size/2)+`,10 `+(this.props.size-10)+`,`+(this.props.size/2)+` `+(this.props.size/2)+`,`+(this.props.size-10)+` "/>
|
||||
</g>
|
||||
`}}),
|
||||
|
||||
|
||||
//left node
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,top:this.props.size/2 - 5}},
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,top:this.props.size/2 - 8, left: -8}},
|
||||
React.createElement(SRD.PortWidget,{name: 'left', node: this.props.node})
|
||||
),
|
||||
|
||||
|
||||
//top node
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left:this.props.size/2-8}},
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left:this.props.size/2-8,top:-8}},
|
||||
React.createElement(SRD.PortWidget,{name: 'top', node: this.props.node})
|
||||
),
|
||||
|
||||
|
||||
//right
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left:this.props.size-10,top:this.props.size/2}},
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left:this.props.size-8,top:this.props.size/2 - 8}},
|
||||
React.createElement(SRD.PortWidget,{name: 'right', node: this.props.node})
|
||||
),
|
||||
|
||||
|
||||
//bottom
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left :this.props.size/2 - 8,top:this.props.size-10}},
|
||||
React.DOM.div({style: {position: 'absolute', zIndex:10,left :this.props.size/2 - 8,top:this.props.size-8}},
|
||||
React.createElement(SRD.PortWidget,{name: 'bottom', node: this.props.node})
|
||||
),
|
||||
)
|
||||
@ -63,4 +63,3 @@ export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, Dia
|
||||
}
|
||||
|
||||
export var DiamonNodeWidgetFactory = React.createFactory(DiamonNodeWidget);
|
||||
|
||||
|
@ -7,41 +7,41 @@ an event on the model itself.
|
||||
|
||||
All models will fire these events:
|
||||
|
||||
#### lockChanged?(entity: BaseEntity,locked: boolean)
|
||||
lockChanged?(entity: BaseEntity,locked: boolean)
|
||||
|
||||
Fires when the lock state of the entity changes. If an element is locked, it cannot be moved or deletes.
|
||||
|
||||
## All Base models excluding DiagramModel
|
||||
|
||||
|
||||
#### selectionChanged?(item: BaseModel, isSelected:boolean)
|
||||
selectionChanged?(item: BaseModel, isSelected:boolean)
|
||||
|
||||
When the _selected_ property of a model changes
|
||||
|
||||
#### entityRemoved?(item:any)
|
||||
|
||||
When the entity is going to be deleted. The DiagramModel listenes for this event to when to remove the model from itself.
|
||||
entityRemoved?(item:any)
|
||||
|
||||
When the entity is going to be deleted. The DiagramModel listeners for this event to when to remove the model from itself.
|
||||
|
||||
|
||||
## DiagramModel
|
||||
|
||||
#### nodesUpdated(node: any, isCreated:boolean)
|
||||
nodesUpdated(node: any, isCreated:boolean)
|
||||
|
||||
When nodes are added or removed
|
||||
|
||||
#### linksUpdated(link: any, isCreated:boolean)
|
||||
|
||||
linksUpdated(link: any, isCreated:boolean)
|
||||
|
||||
when links are added or removed
|
||||
|
||||
#### controlsUpdated() [DEPRECIATED]
|
||||
|
||||
controlsUpdated() [DEPRECIATED]
|
||||
|
||||
_depreciated, use offsetUpdated and zoomUpdated instead_
|
||||
|
||||
#### offsetUpdated(model: DiagramModel,offsetX: number, offsetY: number)
|
||||
|
||||
offsetUpdated(model: DiagramModel,offsetX: number, offsetY: number)
|
||||
|
||||
to know when the canvas was translated in any direction
|
||||
|
||||
#### zoomUpdated(model: DiagramModel,zoom: number)
|
||||
|
||||
zoomUpdated(model: DiagramModel,zoom: number)
|
||||
|
||||
to know when the zoom level of the canvas was updated
|
||||
|
||||
@ -49,21 +49,21 @@ to know when the zoom level of the canvas was updated
|
||||
|
||||
The diagram engine
|
||||
|
||||
#### nodeFactoriesUpdated
|
||||
nodeFactoriesUpdated
|
||||
|
||||
When node factories have been added or removed from the engine
|
||||
|
||||
#### linkFactoriesUpdated
|
||||
linkFactoriesUpdated
|
||||
|
||||
When link factories have been added or removed from the engine
|
||||
|
||||
## LinkModel
|
||||
|
||||
#### sourcePortChanged?(item:LinkModel,target: null|PortModel)
|
||||
|
||||
#### targetPortChanged?(item:LinkModel,target: null|PortModel)
|
||||
sourcePortChanged?(item:LinkModel,target: null|PortModel)
|
||||
|
||||
targetPortChanged?(item:LinkModel,target: null|PortModel)
|
||||
|
||||
|
||||
|
||||
# Example of usage
|
||||
|
||||
```javascript
|
||||
@ -85,4 +85,4 @@ model.addListener({
|
||||
console.log(isAdded?'added':'removed', entity)
|
||||
}
|
||||
});
|
||||
```
|
||||
```
|
||||
|
35
docs/Questions.md
Normal file
@ -0,0 +1,35 @@
|
||||
## Questions
|
||||
|
||||
#### Why didn’t I render the nodes as SVG's?
|
||||
|
||||
Because its vastly better to render nodes as standard HTML so that we can embed input controls and not have
|
||||
to deal with the complexities of trying to get SVG to work like we want it to. I also created this primarily to embed into
|
||||
enterprise applications where the nodes themselves are highly interactive with buttons and other controls that cave when I try to use SVG.
|
||||
|
||||
#### Why Typescript?
|
||||
|
||||
Because it can transpile into any level of ECMA Script, and the library got really complicated, so I ported it to Typescript
|
||||
to accommodate the heavy architectural changes I was starting to make. <3 Type Script
|
||||
|
||||
#### Why is there no JSX?
|
||||
|
||||
Because most of the library is 95% all logic anyway, and I construct very complex DOM elements with many dynamic properties. JSX
|
||||
Would just get in the way, and I personally hate JSX for a multitude of reasons anyway.
|
||||
|
||||
#### But I really want JSX :C
|
||||
|
||||
Changing the internals of this library to be JSX is a pointless endeavor because I built this library in such a way that you never need to hack it to get what you want out of it. If the library is missing something, then I would rather add it as a pluggable feature, or provide some mechanism to allow you to do this.
|
||||
|
||||
Just because this library is NOT JSX, doesn't however mean that you cant use JSX when you want to integrate it into your own system :)
|
||||
|
||||
#### How do I make my own elements?
|
||||
|
||||
Take a look at the __defaults__ directory, with specific attention to the __DefaultNodeWidget__
|
||||
|
||||
That being said, the defaults directory is an EXAMPLE of how you can create your own elements. A number of people want to use the defaults as is, which is cool, but is recommended to create your own base node like the onw you see
|
||||
|
||||
#### How do I use the library?
|
||||
|
||||
Take a look at the demo folders, they have simple and complex examples of the complete usage.
|
||||
|
||||
A good example of a real-world example is Demo 5
|
BIN
images/canvasDrag.gif
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
images/createLink.gif
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
images/createPoint.gif
Normal file
After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
BIN
images/demo3.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
images/example1.png
Normal file
After Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 870 KiB After Width: | Height: | Size: 870 KiB |
BIN
images/example3.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
images/mouseDrag.gif
Normal file
After Width: | Height: | Size: 354 KiB |
BIN
images/mouseWheel.gif
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
images/rjdDelete.gif
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
images/shiftClick.gif
Normal file
After Width: | Height: | Size: 113 KiB |
@ -22,6 +22,7 @@
|
||||
"prepare": "webpack && ./node_modules/node-sass/bin/node-sass --output-style compressed ./src/sass.scss > ./dist/style.css"
|
||||
},
|
||||
"dependencies": {
|
||||
"closest": "^0.0.1",
|
||||
"lodash": "^4.13.1",
|
||||
"react": "^15.1.0"
|
||||
},
|
||||
@ -29,14 +30,14 @@
|
||||
"@types/lodash": "^4.14.52",
|
||||
"@types/react": "^15.0.9",
|
||||
"@types/react-dom": "^0.14.23",
|
||||
"source-map-loader": "^0.1.6",
|
||||
"ts-loader": "^2.0.0",
|
||||
"typescript": "^2.1.6",
|
||||
"css-loader": "^0.26.1",
|
||||
"node-sass": "^4.5.0",
|
||||
"react-dom": "^15.1.0",
|
||||
"sass-loader": "^6.0.0",
|
||||
"source-map-loader": "^0.1.6",
|
||||
"style-loader": "^0.13.1",
|
||||
"ts-loader": "^2.0.0",
|
||||
"typescript": "^2.1.6",
|
||||
"webpack": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
124
src/Common.ts
@ -2,9 +2,9 @@ import {BaseEntity, BaseListener} from "./BaseEntity";
|
||||
import * as _ from "lodash";
|
||||
|
||||
export interface BaseModelListener extends BaseListener{
|
||||
|
||||
|
||||
selectionChanged?(item: BaseModel<BaseModelListener>, isSelected:boolean): void;
|
||||
|
||||
|
||||
entityRemoved?(item:any): void;
|
||||
}
|
||||
|
||||
@ -12,34 +12,34 @@ export interface BaseModelListener extends BaseListener{
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class BaseModel<T extends BaseModelListener> extends BaseEntity<BaseModelListener>{
|
||||
|
||||
|
||||
selected: boolean;
|
||||
|
||||
|
||||
constructor(id?:string){
|
||||
super(id);
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
|
||||
deSerialize(ob){
|
||||
super.deSerialize(ob);
|
||||
this.selected = ob.selected;
|
||||
}
|
||||
|
||||
|
||||
serialize(){
|
||||
return _.merge(super.serialize(),{
|
||||
_class: this.constructor.name,
|
||||
selected: this.selected
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public getID(): string{
|
||||
return this.id
|
||||
}
|
||||
|
||||
|
||||
public isSelected(): boolean{
|
||||
return this.selected;
|
||||
}
|
||||
|
||||
|
||||
public setSelected(selected: boolean = true){
|
||||
this.selected = selected;
|
||||
this.iterateListeners((listener) => {
|
||||
@ -48,7 +48,7 @@ export class BaseModel<T extends BaseModelListener> extends BaseEntity<BaseModel
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remove(){
|
||||
this.iterateListeners((listener) => {
|
||||
if(listener.entityRemoved){
|
||||
@ -59,73 +59,73 @@ export class BaseModel<T extends BaseModelListener> extends BaseEntity<BaseModel
|
||||
}
|
||||
|
||||
export class PointModel extends BaseModel<BaseModelListener>{
|
||||
|
||||
|
||||
x:number;
|
||||
y:number;
|
||||
link: LinkModel;
|
||||
|
||||
|
||||
constructor(link: LinkModel,points: {x:number,y: number}){
|
||||
super();
|
||||
this.x = points.x;
|
||||
this.y = points.y;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
|
||||
deSerialize(ob){
|
||||
super.deSerialize(ob);
|
||||
this.x = ob.x;
|
||||
this.y = ob.y;
|
||||
}
|
||||
|
||||
|
||||
serialize(){
|
||||
return _.merge(super.serialize(),{
|
||||
x: this.x,
|
||||
y: this.y
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remove(){
|
||||
super.remove();
|
||||
|
||||
|
||||
//clear references
|
||||
if (this.link){
|
||||
this.link.removePoint(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateLocation(points: {x:number,y: number}){
|
||||
this.x = points.x;
|
||||
this.y = points.y;
|
||||
}
|
||||
|
||||
|
||||
getX():number{
|
||||
return this.x;
|
||||
}
|
||||
|
||||
|
||||
getY():number{
|
||||
return this.y;
|
||||
}
|
||||
|
||||
|
||||
getLink(): LinkModel{
|
||||
return this.link;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LinkModelListener extends BaseModelListener{
|
||||
|
||||
|
||||
sourcePortChanged?(item:LinkModel,target: null|PortModel): void;
|
||||
|
||||
|
||||
targetPortChanged?(item:LinkModel,target: null|PortModel): void;
|
||||
}
|
||||
|
||||
export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
|
||||
|
||||
linkType: string;
|
||||
sourcePort: PortModel|null;
|
||||
targetPort: PortModel|null;
|
||||
points: PointModel[];
|
||||
extras: {};
|
||||
|
||||
|
||||
constructor(){
|
||||
super();
|
||||
this.linkType = 'default';
|
||||
@ -137,7 +137,7 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
this.sourcePort = null;
|
||||
this.targetPort = null;
|
||||
}
|
||||
|
||||
|
||||
deSerialize(ob){
|
||||
super.deSerialize(ob);
|
||||
this.linkType = ob.type;
|
||||
@ -147,7 +147,7 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
return p;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
serialize(){
|
||||
return _.merge(super.serialize(),{
|
||||
type: this.linkType,
|
||||
@ -161,7 +161,7 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
extras: this.extras
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remove(){
|
||||
super.remove();
|
||||
if (this.sourcePort){
|
||||
@ -171,16 +171,16 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
this.targetPort.removeLink(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isLastPoint(point: PointModel){
|
||||
var index = this.getPointIndex(point);
|
||||
return index === this.points.length-1;
|
||||
}
|
||||
|
||||
|
||||
getPointIndex(point: PointModel){
|
||||
return this.points.indexOf(point);
|
||||
}
|
||||
|
||||
|
||||
getPointModel(id: string): PointModel|null{
|
||||
for (var i = 0; i < this.points.length;i++){
|
||||
if (this.points[i].id === id){
|
||||
@ -189,15 +189,15 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
getFirstPoint(): PointModel{
|
||||
return this.points[0];
|
||||
}
|
||||
|
||||
|
||||
getLastPoint(): PointModel{
|
||||
return this.points[this.points.length-1];
|
||||
}
|
||||
|
||||
|
||||
setSourcePort(port: PortModel){
|
||||
port.addLink(this);
|
||||
this.sourcePort = port;
|
||||
@ -205,15 +205,15 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
listener.sourcePortChanged && listener.sourcePortChanged(this, port);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getSourcePort(): PortModel{
|
||||
return this.sourcePort;
|
||||
}
|
||||
|
||||
|
||||
getTargetPort(): PortModel{
|
||||
return this.targetPort;
|
||||
}
|
||||
|
||||
|
||||
setTargetPort(port: PortModel){
|
||||
port.addLink(this);
|
||||
this.targetPort = port;
|
||||
@ -221,23 +221,23 @@ export class LinkModel extends BaseModel<LinkModelListener>{
|
||||
listener.targetPortChanged && listener.targetPortChanged(this, port);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getPoints(): PointModel[]{
|
||||
return this.points;
|
||||
}
|
||||
|
||||
|
||||
setPoints(points: PointModel[]){
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
|
||||
removePoint(pointModel: PointModel){
|
||||
this.points.splice(this.getPointIndex(pointModel),1);
|
||||
}
|
||||
|
||||
|
||||
addPoint(pointModel:PointModel,index = 1){
|
||||
this.points.splice(index,0,pointModel);
|
||||
}
|
||||
|
||||
|
||||
getType(): string{
|
||||
return this.linkType;
|
||||
}
|
||||
@ -247,12 +247,12 @@ export class PortModel extends BaseModel<BaseModelListener>{
|
||||
name: string;
|
||||
parentNode: NodeModel;
|
||||
links: {[id: string]: LinkModel};
|
||||
|
||||
|
||||
deSerialize(ob){
|
||||
super.deSerialize(ob);
|
||||
this.name = ob.name;
|
||||
}
|
||||
|
||||
|
||||
serialize(){
|
||||
return _.merge(super.serialize(),{
|
||||
name: this.name,
|
||||
@ -262,47 +262,47 @@ export class PortModel extends BaseModel<BaseModelListener>{
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
constructor(name: string, id?:string){
|
||||
super(id);
|
||||
this.name = name;
|
||||
this.links = {};
|
||||
this.parentNode = null;
|
||||
}
|
||||
|
||||
|
||||
getName(): string{
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
getParent(): NodeModel{
|
||||
return this.parentNode;
|
||||
}
|
||||
|
||||
|
||||
setParentNode(node: NodeModel){
|
||||
this.parentNode = node;
|
||||
}
|
||||
|
||||
|
||||
removeLink(link: LinkModel){
|
||||
delete this.links[link.getID()];
|
||||
}
|
||||
|
||||
|
||||
addLink(link: LinkModel){
|
||||
this.links[link.getID()] = link;
|
||||
}
|
||||
|
||||
|
||||
getLinks(): {[id: string]: LinkModel}{
|
||||
return this.links;
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
|
||||
|
||||
nodeType: string;
|
||||
x: number;
|
||||
y: number;
|
||||
extras: {};
|
||||
ports: {[s: string]:PortModel};
|
||||
|
||||
|
||||
constructor(nodeType: string = 'default', id?:string){
|
||||
super(id);
|
||||
this.nodeType = nodeType;
|
||||
@ -311,7 +311,7 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
this.extras = {};
|
||||
this.ports = {};
|
||||
}
|
||||
|
||||
|
||||
deSerialize(ob){
|
||||
super.deSerialize(ob);
|
||||
this.nodeType = ob.type;
|
||||
@ -319,7 +319,7 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
this.y = ob.y;
|
||||
this.extras = ob.extras;
|
||||
}
|
||||
|
||||
|
||||
serialize(){
|
||||
return _.merge(super.serialize(),{
|
||||
type: this.nodeType,
|
||||
@ -331,7 +331,7 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
remove(){
|
||||
super.remove();
|
||||
for (var i in this.ports){
|
||||
@ -340,7 +340,7 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getPortFromID(id): PortModel | null{
|
||||
for (var i in this.ports){
|
||||
if (this.ports[i].id === id){
|
||||
@ -349,15 +349,15 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
getPort(name: string): PortModel | null{
|
||||
return this.ports[name];
|
||||
}
|
||||
|
||||
|
||||
getPorts(): {[s: string]:PortModel}{
|
||||
return this.ports;
|
||||
}
|
||||
|
||||
|
||||
removePort(port: PortModel){
|
||||
//clear the parent node reference
|
||||
if(this.ports[port.name]){
|
||||
@ -365,14 +365,14 @@ export class NodeModel extends BaseModel<BaseModelListener>{
|
||||
delete this.ports[port.name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addPort(port: PortModel): PortModel{
|
||||
port.setParentNode(this);
|
||||
this.ports[port.name] = port;
|
||||
return port;
|
||||
}
|
||||
|
||||
|
||||
getType(): string{
|
||||
return this.nodeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import closest = require("closest");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
@ -12,4 +13,17 @@ export class Toolkit {
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest element as a polyfill
|
||||
*
|
||||
* @param {Element} element [description]
|
||||
* @param {string} selector [description]
|
||||
*/
|
||||
public static closest(element: Element,selector:string){
|
||||
if(document.body.closest){
|
||||
return element.closest(selector);
|
||||
}
|
||||
return closest(element,selector);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import * as _ from "lodash";
|
||||
import {PointModel, NodeModel, BaseModel, BaseModelListener, LinkModel,PortModel} from "../Common";
|
||||
import {LinkLayerWidget} from "./LinkLayerWidget";
|
||||
import {NodeLayerWidget} from "./NodeLayerWidget";
|
||||
import {Toolkit} from "../Toolkit";
|
||||
|
||||
export interface SelectionModel{
|
||||
model: BaseModel<BaseModelListener>;
|
||||
@ -16,7 +17,7 @@ export class BaseAction{
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
ms: number;
|
||||
|
||||
|
||||
constructor(mouseX: number,mouseY: number){
|
||||
this.mouseX = mouseX;
|
||||
this.mouseY = mouseY;
|
||||
@ -27,19 +28,19 @@ export class BaseAction{
|
||||
export class SelectingAction extends BaseAction{
|
||||
mouseX2: number;
|
||||
mouseY2: number;
|
||||
|
||||
|
||||
constructor(mouseX: number,mouseY: number){
|
||||
super(mouseX,mouseY);
|
||||
this.mouseX2 = mouseX;
|
||||
this.mouseY2 = mouseY;
|
||||
}
|
||||
|
||||
|
||||
containsElement(x: number, y: number, diagramModel: DiagramModel): boolean{
|
||||
var z = diagramModel.getZoomLevel()/100.0;
|
||||
return (
|
||||
(x + diagramModel.getOffsetX())*z > this.mouseX &&
|
||||
(x + diagramModel.getOffsetX())*z < this.mouseX2&&
|
||||
(y + diagramModel.getOffsetY())*z > this.mouseY &&
|
||||
(x + diagramModel.getOffsetX())*z > this.mouseX &&
|
||||
(x + diagramModel.getOffsetX())*z < this.mouseX2&&
|
||||
(y + diagramModel.getOffsetY())*z > this.mouseY &&
|
||||
(y + diagramModel.getOffsetY())*z < this.mouseY2
|
||||
)
|
||||
}
|
||||
@ -48,7 +49,7 @@ export class SelectingAction extends BaseAction{
|
||||
export class MoveCanvasAction extends BaseAction{
|
||||
initialOffsetX: number;
|
||||
initialOffsetY: number;
|
||||
|
||||
|
||||
constructor(mouseX: number, mouseY: number, diagramModel: DiagramModel){
|
||||
super(mouseX,mouseY);
|
||||
this.initialOffsetX = diagramModel.getOffsetX();
|
||||
@ -64,12 +65,12 @@ export class MoveItemsAction extends BaseAction{
|
||||
this.moved = false;
|
||||
diagramEngine.enableRepaintEntities(diagramEngine.getDiagramModel().getSelectedItems());
|
||||
var selectedItems = diagramEngine.getDiagramModel().getSelectedItems();
|
||||
|
||||
|
||||
//dont allow items which are locked to move
|
||||
selectedItems = selectedItems.filter((item) => {
|
||||
return !diagramEngine.isModelLocked(item);
|
||||
})
|
||||
|
||||
|
||||
this.selectionModels = selectedItems.map((item: PointModel | NodeModel) => {
|
||||
return {
|
||||
model: item,
|
||||
@ -82,11 +83,11 @@ export class MoveItemsAction extends BaseAction{
|
||||
|
||||
export interface DiagramProps {
|
||||
diagramEngine:DiagramEngine;
|
||||
|
||||
|
||||
allowLooseLinks?: boolean;
|
||||
allowCanvasTranslation?: boolean;
|
||||
allowCanvasZoom?: boolean;
|
||||
|
||||
|
||||
actionStartedFiring?: (action: BaseAction) => boolean;
|
||||
actionStillFiring?: (action: BaseAction) => void;
|
||||
actionStoppedFiring?: (action: BaseAction) => void;
|
||||
@ -105,7 +106,7 @@ export interface DiagramState {
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
|
||||
|
||||
public static defaultProps: DiagramProps = {
|
||||
diagramEngine:null,
|
||||
allowLooseLinks: true,
|
||||
@ -126,7 +127,7 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
document:null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentWillMount(){
|
||||
this.setState({
|
||||
diagramEngineListener: this.props.diagramEngine.addListener({
|
||||
@ -144,7 +145,7 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
window.removeEventListener('mouseUp',this.onMouseUp);
|
||||
window.removeEventListener('mouseMove',this.onMouseMove);
|
||||
}
|
||||
|
||||
|
||||
componentWillUpdate(nextProps: DiagramProps){
|
||||
if (this.props.diagramEngine.diagramModel.id !== nextProps.diagramEngine.diagramModel.id){
|
||||
this.setState({renderedNodes: false});
|
||||
@ -155,7 +156,7 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
nextProps.diagramEngine.diagramModel.rendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(){
|
||||
if (!this.state.renderedNodes){
|
||||
this.setState({
|
||||
@ -163,10 +164,10 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount(){
|
||||
this.props.diagramEngine.setCanvas(this.refs['canvas'] as Element);
|
||||
|
||||
|
||||
//add a keyboard listener
|
||||
this.setState({
|
||||
document: document,
|
||||
@ -187,67 +188,67 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
});
|
||||
window.focus();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a model and element under the mouse cursor
|
||||
*/
|
||||
getMouseElement(event): {model: BaseModel<BaseModelListener>, element: Element}{
|
||||
var target = event.target as Element;
|
||||
var diagramModel = this.props.diagramEngine.diagramModel;
|
||||
|
||||
|
||||
//is it a port
|
||||
var element = target.closest('.port[data-name]');
|
||||
var element = Toolkit.closest(target,'.port[data-name]');
|
||||
if(element){
|
||||
var nodeElement = target.closest('.node[data-nodeid]') as HTMLElement;
|
||||
var nodeElement = Toolkit.closest(target,'.node[data-nodeid]') as HTMLElement;
|
||||
return {
|
||||
model: diagramModel.getNode(nodeElement.getAttribute('data-nodeid')).getPort(element.getAttribute('data-name')),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//look for a point
|
||||
element = target.closest('.point[data-id]');
|
||||
element = Toolkit.closest(target,'.point[data-id]');
|
||||
if(element){
|
||||
return {
|
||||
model: diagramModel.getLink(element.getAttribute('data-linkid')).getPointModel(element.getAttribute('data-id')),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//look for a link
|
||||
element = target.closest('[data-linkid]');
|
||||
element = Toolkit.closest(target,'[data-linkid]');
|
||||
if(element){
|
||||
return {
|
||||
model: diagramModel.getLink(element.getAttribute('data-linkid')),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//look for a node
|
||||
element = target.closest('.node[data-nodeid]');
|
||||
element = Toolkit.closest(target,'.node[data-nodeid]');
|
||||
if(element){
|
||||
return {
|
||||
model: diagramModel.getNode(element.getAttribute('data-nodeid')),
|
||||
element: element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
fireAction(){
|
||||
if (this.state.action && this.props.actionStillFiring){
|
||||
this.props.actionStillFiring(this.state.action);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stopFiringAction(shouldSkipEvent?:boolean){
|
||||
if (this.props.actionStoppedFiring && !shouldSkipEvent){
|
||||
this.props.actionStoppedFiring(this.state.action);
|
||||
}
|
||||
this.setState({action:null});
|
||||
}
|
||||
|
||||
|
||||
startFiringAction(action: BaseAction){
|
||||
var setState = true;
|
||||
if(this.props.actionStartedFiring){
|
||||
@ -373,7 +374,7 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
render() {
|
||||
var diagramEngine = this.props.diagramEngine;
|
||||
var diagramModel = diagramEngine.getDiagramModel();
|
||||
|
||||
|
||||
return (
|
||||
React.DOM.div({
|
||||
ref:'canvas',
|
||||
@ -404,27 +405,27 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
var relative = diagramEngine.getRelativePoint(event.pageX, event.pageY);
|
||||
this.startFiringAction(new SelectingAction(relative.x, relative.y));
|
||||
}
|
||||
|
||||
|
||||
//its a drag the canvas event
|
||||
else{
|
||||
diagramModel.clearSelection();
|
||||
this.startFiringAction(new MoveCanvasAction(event.pageX, event.pageY, diagramModel));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//its a port element, we want to drag a link
|
||||
else if (model.model instanceof PortModel){
|
||||
var relative = diagramEngine.getRelativeMousePoint(event);
|
||||
var link = new LinkModel();
|
||||
link.setSourcePort(model.model);
|
||||
|
||||
|
||||
link.getFirstPoint().updateLocation(relative)
|
||||
link.getLastPoint().updateLocation(relative);
|
||||
|
||||
|
||||
diagramModel.clearSelection();
|
||||
link.getLastPoint().setSelected(true);
|
||||
diagramModel.addLink(link);
|
||||
|
||||
|
||||
this.startFiringAction(new MoveItemsAction(event.pageX, event.pageY, diagramEngine));
|
||||
}
|
||||
//its some or other element, probably want to move it
|
||||
@ -433,7 +434,7 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
diagramModel.clearSelection();
|
||||
}
|
||||
model.model.setSelected(true);
|
||||
|
||||
|
||||
this.startFiringAction(new MoveItemsAction(event.pageX, event.pageY,diagramEngine));
|
||||
}
|
||||
this.state.document.addEventListener('mousemove', this.onMouseMove);
|
||||
@ -465,4 +466,4 @@ export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.spec.ts",
|
||||
"node_modules",
|
||||
"**/*.spec.ts",
|
||||
"webpack.config.js",
|
||||
"./dist"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,22 @@
|
||||
var webpack = require("webpack");
|
||||
var plugins = [];
|
||||
|
||||
//do we minify it all
|
||||
if(process.env.NODE_ENV === 'production'){
|
||||
plugins.push(new webpack.optimize.UglifyJsPlugin({
|
||||
mangle: {
|
||||
keep_fnames: true
|
||||
},
|
||||
compress: {
|
||||
keep_fnames: true,
|
||||
warnings: false,
|
||||
}
|
||||
}));
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
@ -32,20 +50,7 @@ module.exports = [
|
||||
root: '_'
|
||||
}
|
||||
},
|
||||
plugins:[
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
mangle: {
|
||||
keep_fnames: true
|
||||
},
|
||||
compress: {
|
||||
keep_fnames: true,
|
||||
warnings: false,
|
||||
}
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
],
|
||||
plugins:plugins,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@ -64,7 +69,7 @@ module.exports = [
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"]
|
||||
},
|
||||
// devtool: 'source-map'
|
||||
devtool: 'eval-cheap-module-source-map'
|
||||
},
|
||||
//for building the demos and tests
|
||||
{
|
||||
@ -101,20 +106,7 @@ module.exports = [
|
||||
root: '_'
|
||||
}
|
||||
},
|
||||
plugins:[
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
mangle: {
|
||||
keep_fnames: true
|
||||
},
|
||||
compress: {
|
||||
keep_fnames: true,
|
||||
warnings: false,
|
||||
}
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
],
|
||||
plugins:plugins,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@ -146,6 +138,6 @@ module.exports = [
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"]
|
||||
},
|
||||
// devtool: 'source-map'
|
||||
devtool: 'eval-cheap-module-source-map'
|
||||
}
|
||||
];
|
||||
|
10
yarn.lock
@ -433,6 +433,12 @@ clone@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
|
||||
|
||||
closest@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/closest/-/closest-0.0.1.tgz#26da6f80b3e0e17e71f80f12782819e9f653495c"
|
||||
dependencies:
|
||||
matches-selector "0.0.1"
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
@ -1427,6 +1433,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||
|
||||
matches-selector@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/matches-selector/-/matches-selector-0.0.1.tgz#1df5262243ae341c1a0804dd302048267ac713bb"
|
||||
|
||||
math-expression-evaluator@^1.2.14:
|
||||
version "1.2.16"
|
||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9"
|
||||
|