mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2026-03-13 09:50:09 +08:00
First Import
This commit is contained in:
186
.gitignore
vendored
Normal file
186
.gitignore
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
|
||||
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node
|
||||
|
||||
#!! ERROR: net is undefined. Use list command to see defined gitignore types !!#
|
||||
|
||||
### NetBeans ###
|
||||
nbproject/private/
|
||||
build/
|
||||
nbbuild/
|
||||
dist/
|
||||
nbdist/
|
||||
nbactions.xml
|
||||
.nb-gradle/
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
# *.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
|
||||
### PhpStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### PhpStorm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
|
||||
|
||||
### Windows ###
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
|
||||
### OSX ###
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
7
nbproject/project.properties
Normal file
7
nbproject/project.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
auxiliary.org-netbeans-modules-css-prep.sass_2e_compiler_2e_options=
|
||||
auxiliary.org-netbeans-modules-css-prep.sass_2e_configured=true
|
||||
auxiliary.org-netbeans-modules-css-prep.sass_2e_enabled=false
|
||||
auxiliary.org-netbeans-modules-css-prep.sass_2e_mappings=/scss:/css
|
||||
files.encoding=UTF-8
|
||||
site.root.folder=
|
||||
source.folder=
|
||||
9
nbproject/project.xml
Normal file
9
nbproject/project.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||
<type>org.netbeans.modules.web.clientproject</type>
|
||||
<configuration>
|
||||
<data xmlns="http://www.netbeans.org/ns/clientside-project/1">
|
||||
<name>storm-react-flow-2</name>
|
||||
</data>
|
||||
</configuration>
|
||||
</project>
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "storm-react-flow-2",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"util",
|
||||
"functional",
|
||||
"server",
|
||||
"client",
|
||||
"browser"
|
||||
],
|
||||
"main": "src/main.js",
|
||||
"author": "dylanvorster",
|
||||
"contributors": [],
|
||||
"dependencies": {
|
||||
"color": "^0.11.1",
|
||||
"react": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^0.23.1",
|
||||
"node-sass": "^3.7.0",
|
||||
"react-dom": "^15.1.0",
|
||||
"sass-loader": "^3.2.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"webpack": "^1.13.1"
|
||||
}
|
||||
}
|
||||
181
src/Engine.js
Normal file
181
src/Engine.js
Normal file
@@ -0,0 +1,181 @@
|
||||
var _ = require("lodash");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = function(){
|
||||
return {
|
||||
state:{
|
||||
links:{},
|
||||
nodes:{},
|
||||
factories: {},
|
||||
canvas: null,
|
||||
offsetX:0,
|
||||
offsetY:0,
|
||||
zoom: 100,
|
||||
listeners:{},
|
||||
selectedLink: null
|
||||
},
|
||||
|
||||
update: function(){
|
||||
this.fireEvent({type:'repaint'});
|
||||
},
|
||||
|
||||
getRelativeMousePoint: function(event){
|
||||
return this.getRelativePoint((event.pageX/(this.state.zoom/100.0))-this.state.offsetX,(event.pageY/(this.state.zoom/100.0))-this.state.offsetY);
|
||||
},
|
||||
|
||||
getRelativePoint: function(x,y){
|
||||
var canvasRect = this.state.canvas.getBoundingClientRect();
|
||||
return {x: x-canvasRect.left,y:y-canvasRect.top};
|
||||
},
|
||||
|
||||
fireEvent: function(event){
|
||||
_.forEach(this.state.listeners,function(listener){
|
||||
listener(event);
|
||||
});
|
||||
},
|
||||
|
||||
removeListener: function(id){
|
||||
delete this.state.listeners[id];
|
||||
},
|
||||
|
||||
registerListener: function(cb){
|
||||
var id = this.UID();
|
||||
this.state.listeners[id] = cb;
|
||||
return id;
|
||||
},
|
||||
|
||||
setZoom: function(zoom){
|
||||
this.state.zoom = zoom;
|
||||
},
|
||||
|
||||
setOffset: function(x,y){
|
||||
this.state.offsetX = x;
|
||||
this.state.offsetY = y;
|
||||
},
|
||||
|
||||
loadModel: function(model){
|
||||
this.state.links = {};
|
||||
this.state.node = {};
|
||||
|
||||
model.nodes.forEach(function(node){
|
||||
this.addNode(node);
|
||||
}.bind(this));
|
||||
|
||||
model.links.forEach(function(link){
|
||||
this.addLink(link);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
updateNode: function(node){
|
||||
//find the links and move those as well
|
||||
},
|
||||
|
||||
UID: function(){
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
},
|
||||
|
||||
getNodePortElement: function(node,port){
|
||||
return this.state.canvas.querySelector('.port[data-name="'+port+'"][data-nodeid="'+node.id+'"]')
|
||||
},
|
||||
|
||||
getAllConnectedPorts: function(node){
|
||||
return this.state.canvas.querySelectorAll('.port[data-nodeid="'+node.id+'"]');
|
||||
},
|
||||
|
||||
getNodeLinks: function(node){
|
||||
return _.values(_.filter(this.state.links,function(link,index){
|
||||
return link.source == node.id || link.target == node.id;
|
||||
}));
|
||||
},
|
||||
|
||||
removeLink: function(link){
|
||||
delete this.state.links[link.id];
|
||||
this.update();
|
||||
},
|
||||
|
||||
removeNode: function(node){
|
||||
//remove the links
|
||||
var links = this.getNodeLinks(node);
|
||||
links.forEach(function(link){
|
||||
this.removeLink(link);
|
||||
}.bind(this));
|
||||
|
||||
//remove the node
|
||||
delete this.state.nodes[node.id];
|
||||
this.update();
|
||||
},
|
||||
|
||||
getPortCenter: function(node,port){
|
||||
var sourceElement = this.getNodePortElement(node,port);
|
||||
var sourceRect = sourceElement.getBoundingClientRect();
|
||||
|
||||
var rel = this.getRelativePoint(sourceRect.left,sourceRect.top);
|
||||
|
||||
return {
|
||||
x: (sourceElement.offsetWidth/2) + rel.x,
|
||||
y: (sourceElement.offsetHeight/2) + rel.y
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
setSelectedLink: function(link){
|
||||
this.state.selectedLink = link;
|
||||
},
|
||||
|
||||
addLink: function(link){
|
||||
var DefaultLink = {
|
||||
id: this.UID(),
|
||||
source: null,
|
||||
sourcePort: null,
|
||||
target: null,
|
||||
targetPort: null,
|
||||
points: []
|
||||
};
|
||||
var FinalLink = _.merge(DefaultLink,link);
|
||||
|
||||
this.state.links[FinalLink.id] = FinalLink;
|
||||
return FinalLink;
|
||||
},
|
||||
|
||||
addNode: function(node){
|
||||
var DefaultNode = {
|
||||
id: this.UID(),
|
||||
type: 'default',
|
||||
data:{}
|
||||
};
|
||||
var FinalNode = _.merge(DefaultNode,node);
|
||||
this.state.nodes[FinalNode.id] = FinalNode;
|
||||
},
|
||||
|
||||
getLink: function(id){
|
||||
return this.state.links[id];
|
||||
},
|
||||
|
||||
getNode: function(id){
|
||||
return this.state.nodes[id];
|
||||
},
|
||||
|
||||
getNodeFactory: function(type){
|
||||
if(this.state.factories[type] === undefined){
|
||||
throw "Cannot find node factory for: "+type;
|
||||
}
|
||||
return this.state.factories[type];
|
||||
},
|
||||
|
||||
registerNodeFactory: function(factory){
|
||||
var DefaultFactory = {
|
||||
type: "factoty",
|
||||
generateModel: function(model){
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var FinalModel = _.merge(DefaultFactory,factory);
|
||||
this.state.factories[FinalModel.type] = FinalModel;
|
||||
}
|
||||
};
|
||||
};
|
||||
3
src/main.js
Normal file
3
src/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
|
||||
};
|
||||
123
src/sass.scss
Normal file
123
src/sass.scss
Normal file
@@ -0,0 +1,123 @@
|
||||
.storm-flow-canvas{
|
||||
top:0px;
|
||||
left:0px;
|
||||
bottom:0;
|
||||
right:0;
|
||||
position: absolute;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
svg{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.node-view{
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.node{
|
||||
position: absolute;
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
from {
|
||||
stroke-dashoffset: 24;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
path{
|
||||
fill:none;
|
||||
stroke:black;
|
||||
pointer-events:all;
|
||||
|
||||
&.selected{
|
||||
stroke: rgb(0,192,255) !important;
|
||||
stroke-dasharray: 10,2;
|
||||
animation: dash 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.port{
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: rgba(white,0.1);
|
||||
&:hover,&.selected{
|
||||
background: rgb(192,255,0);
|
||||
}
|
||||
}
|
||||
|
||||
.basic-node{
|
||||
background-color: rgb(30,30,30);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(black,0.5);
|
||||
font-family:Arial;
|
||||
color: white;
|
||||
border: solid 1px black;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
|
||||
.title{
|
||||
/* background-image: linear-gradient(rgba(black,0.1),rgba(black,0.2));*/
|
||||
background: rgba(black,0.3);
|
||||
display: flex;
|
||||
>*{
|
||||
align-self: center;
|
||||
}
|
||||
.fa{
|
||||
padding: 5px;
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover{
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
.name{
|
||||
flex-grow: 1;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ports{
|
||||
display: flex;
|
||||
background-image: linear-gradient(rgba(black,0.1),rgba(black,0.2));
|
||||
.in, .out{
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.in-port,.out-port{
|
||||
display: flex;
|
||||
margin-top: 1px;
|
||||
>*{
|
||||
align-self: center;
|
||||
}
|
||||
.name{
|
||||
flex-grow:1;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
.out-port{
|
||||
|
||||
.name{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/widgets/BasicNodeWidget.js
Normal file
48
src/widgets/BasicNodeWidget.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var React = require("react");
|
||||
var Port = require("./PortWidget");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "BasicNodeWidget",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
name: "Node",
|
||||
node: null,
|
||||
inPorts:[],
|
||||
outPorts: [],
|
||||
color: 'rgb(50,50,50)',
|
||||
removeAction: function(){
|
||||
console.log("remove node");
|
||||
}
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.div({className:'basic-node', style: {background:this.props.color }},
|
||||
React.DOM.div({className:'title'},
|
||||
React.DOM.div({className:'name'},this.props.name),
|
||||
React.DOM.div({className:'fa fa-close',onClick: this.props.removeAction})
|
||||
),
|
||||
React.DOM.div({className:'ports'},
|
||||
React.DOM.div({className:'in'},this.props.inPorts.map(function(port){
|
||||
return React.DOM.div({className:'in-port'},
|
||||
React.createElement(Port,{name:port,node:this.props.node}),
|
||||
React.DOM.div({className:'name'},port)
|
||||
);
|
||||
}.bind(this))),
|
||||
React.DOM.div({className:'out'},this.props.outPorts.map(function(port){
|
||||
return React.DOM.div({className:'out-port'},
|
||||
React.DOM.div({className:'name'},port),
|
||||
React.createElement(Port,{name:port,node:this.props.node})
|
||||
);
|
||||
}.bind(this)))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
196
src/widgets/CanvasWidget.js
Normal file
196
src/widgets/CanvasWidget.js
Normal file
@@ -0,0 +1,196 @@
|
||||
var React = require("react");
|
||||
var SVGWidget = require("./SVGWidget");
|
||||
var NodeView = require("./NodeViewWidget");
|
||||
var _ = require("lodash");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "CanvasWidget",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
selectedPointID: null,
|
||||
selectedLink: null,
|
||||
selectedModel: null,
|
||||
initialX: null,
|
||||
initialY: null,
|
||||
initialObjectX: null,
|
||||
initialObjectY: null,
|
||||
listenerID: null
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
engine: null
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function(){
|
||||
this.props.engine.removeListener(this.state.listenerID);
|
||||
},
|
||||
|
||||
componentDidMount: function(){
|
||||
this.props.engine.state.canvas = this.refs.canvas;
|
||||
var listenerID = this.props.engine.registerListener(function(event){
|
||||
if(event.type === 'repaint'){
|
||||
this.forceUpdate();
|
||||
}
|
||||
}.bind(this));
|
||||
this.setState({listenerID: listenerID});
|
||||
|
||||
setTimeout(function(){
|
||||
//check for any links that dont have points
|
||||
_.forEach(this.props.engine.state.links,function(link){
|
||||
if(link.points.length === 0){
|
||||
link.points.push(this.props.engine.getPortCenter(this.props.engine.getNode(link.source),link.sourcePort));
|
||||
link.points.push(this.props.engine.getPortCenter(this.props.engine.getNode(link.target),link.targetPort));
|
||||
this.forceUpdate();
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this),10);
|
||||
|
||||
|
||||
//add a keybaord listener
|
||||
window.addEventListener('keydown',function(){
|
||||
if(this.props.engine.state.selectedLink){
|
||||
this.props.engine.removeLink(this.props.engine.state.selectedLink);
|
||||
}
|
||||
}.bind(this));
|
||||
window.focus();
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.div({
|
||||
style:{
|
||||
zoom: this.props.engine.state.zoom+"%",
|
||||
},
|
||||
ref:'canvas',
|
||||
className:'storm-flow-canvas',
|
||||
onWheel: function(event){
|
||||
this.props.engine.setZoom(this.props.engine.state.zoom+(event.deltaY/60));
|
||||
this.forceUpdate();
|
||||
}.bind(this),
|
||||
onMouseMove: function(event){
|
||||
|
||||
//move the node
|
||||
if(this.state.selectedModel){
|
||||
this.state.selectedModel.x = this.state.initialObjectX+((event.pageX-this.state.initialX)/(this.props.engine.state.zoom/100));
|
||||
this.state.selectedModel.y = this.state.initialObjectY+((event.pageY-this.state.initialY)/(this.props.engine.state.zoom/100));
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
//move the point
|
||||
else if(this.state.selectedPointID){
|
||||
var point = _.find(this.state.selectedLink.points,{id:this.state.selectedPointID});
|
||||
var rel = this.props.engine.getRelativeMousePoint(event);
|
||||
point.x = rel.x;
|
||||
point.y = rel.y;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
//move the canvas
|
||||
else if(this.state.initialObjectX !== null){
|
||||
this.props.engine.setOffset(
|
||||
this.state.initialObjectX+((event.pageX-this.state.initialX)/(this.props.engine.state.zoom/100)),
|
||||
this.state.initialObjectY+((event.pageY-this.state.initialY)/(this.props.engine.state.zoom/100))
|
||||
);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}.bind(this),
|
||||
onMouseDown: function(event){
|
||||
|
||||
//look for a port
|
||||
var element = event.target.closest('.port[data-name]');
|
||||
if(element){
|
||||
var nodeElement = event.target.closest('.node[data-nodeid]');
|
||||
var rel = this.props.engine.getRelativeMousePoint(event);
|
||||
var id = this.props.engine.UID();
|
||||
var FinalLink = this.props.engine.addLink({
|
||||
source: nodeElement.dataset.nodeid,
|
||||
sourcePort: element.dataset.name,
|
||||
points:[{x:0,y:0},{x:rel.x,y:rel.y,id: id}]
|
||||
});
|
||||
this.setState({
|
||||
selectedPointID: id,
|
||||
selectedLink: FinalLink
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//look for a point
|
||||
element = event.target.closest('.point[data-id]');
|
||||
if(element){
|
||||
|
||||
//chrome fix o_O
|
||||
if(element.dataset === undefined){
|
||||
element.dataset = {
|
||||
id:element.getAttribute('data-id'),
|
||||
linkid: element.getAttribute('data-linkid')
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
selectedPointID: element.dataset.id,
|
||||
selectedLink: this.props.engine.getLink(element.dataset.linkid)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//look for an element
|
||||
element = event.target.closest('.node[data-nodeid]');
|
||||
if(element){
|
||||
var model = this.props.engine.getNode(element.dataset['nodeid']);
|
||||
this.setState({
|
||||
selectedModel: model,
|
||||
initialX: event.pageX,
|
||||
initialY: event.pageY,
|
||||
initialObjectX: model.x,
|
||||
initialObjectY: model.y
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//probably just the canvas
|
||||
this.setState({
|
||||
initialX: event.pageX,
|
||||
initialY: event.pageY,
|
||||
initialObjectX: this.props.engine.state.offsetX,
|
||||
initialObjectY: this.props.engine.state.offsetY
|
||||
});
|
||||
|
||||
|
||||
}.bind(this),
|
||||
onMouseUp: function(event){
|
||||
|
||||
if(this.state.selectedPointID){
|
||||
var element = event.target.closest('.port[data-name]');
|
||||
if(element){
|
||||
var nodeElement = event.target.closest('.node[data-nodeid]');
|
||||
this.state.selectedLink.target = nodeElement.dataset.nodeid;
|
||||
this.state.selectedLink.targetPort = element.dataset.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
selectedLink: null,
|
||||
selectedPort: null,
|
||||
selectedPointID: null,
|
||||
selectedModel: null,
|
||||
initialX: null,
|
||||
initialY: null,
|
||||
initialObjectX: null,
|
||||
initialObjectY: null
|
||||
});
|
||||
}.bind(this),
|
||||
},
|
||||
React.createElement(SVGWidget,{newPoint: function(link,pointID){
|
||||
this.setState({
|
||||
selectedPointID: pointID,
|
||||
selectedLink: link
|
||||
});;
|
||||
}.bind(this),engine: this.props.engine}),
|
||||
React.createElement(NodeView,{engine: this.props.engine})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
172
src/widgets/LinkWidget.js
Normal file
172
src/widgets/LinkWidget.js
Normal file
@@ -0,0 +1,172 @@
|
||||
var React = require("react");
|
||||
var _ = require("lodash");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "LinkWidget",
|
||||
getInitialState: function(){
|
||||
return {
|
||||
selected: false,
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
width: 3,
|
||||
link:null,
|
||||
engine: null,
|
||||
smooth: false,
|
||||
newPoint: function(id){
|
||||
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getPoint: function(index){
|
||||
if(index === 0){
|
||||
return this.props.link.points[index];
|
||||
}
|
||||
if(this.props.link.target !== null && index === this.props.link.points.length-1){
|
||||
return this.props.link.points[index];
|
||||
}
|
||||
return {
|
||||
x: this.props.link.points[index].x+this.props.engine.state.offsetX,
|
||||
y: this.props.link.points[index].y+this.props.engine.state.offsetY
|
||||
};
|
||||
},
|
||||
|
||||
setSelected: function(selected){
|
||||
this.setState({selected: selected});
|
||||
if(selected){
|
||||
this.props.engine.setSelectedLink(selected?this.props.link:null);
|
||||
}
|
||||
},
|
||||
|
||||
generatePoint: function(pointIndex){
|
||||
return React.DOM.g(null,
|
||||
React.DOM.circle({
|
||||
className:'point',
|
||||
cx:this.getPoint(pointIndex).x,
|
||||
cy:this.getPoint(pointIndex).y,
|
||||
r:5,
|
||||
fill:'black',
|
||||
}),
|
||||
React.DOM.circle({
|
||||
className:'point',
|
||||
'data-linkid':this.props.link.id,
|
||||
'data-id':this.props.link.points[pointIndex].id,
|
||||
cx:this.getPoint(pointIndex).x,
|
||||
cy:this.getPoint(pointIndex).y,
|
||||
r:15,
|
||||
opacity: 0,
|
||||
onMouseLeave: function(){
|
||||
this.setSelected(false);
|
||||
}.bind(this),
|
||||
onMouseEnter: function(){
|
||||
this.setSelected(true);
|
||||
}.bind(this)
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
generateLink: function(extraProps){
|
||||
var Bottom = React.DOM.path(_.merge({
|
||||
className:this.state.selected?'selected':'',
|
||||
strokeWidth:this.props.width,
|
||||
stroke:'black'
|
||||
},extraProps));
|
||||
|
||||
var Top = React.DOM.path(_.merge({
|
||||
onMouseLeave: function(){
|
||||
this.setSelected(false);
|
||||
}.bind(this),
|
||||
onMouseEnter: function(){
|
||||
this.setSelected(true);
|
||||
}.bind(this),
|
||||
strokeOpacity:0,
|
||||
strokeWidth: 20,
|
||||
onContextMenu: function(event){
|
||||
event.preventDefault();
|
||||
this.props.engine.removeLink(this.props.link);
|
||||
}.bind(this),
|
||||
},extraProps));
|
||||
|
||||
return React.DOM.g(null,
|
||||
Bottom,
|
||||
Top
|
||||
);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var points = this.props.link.points;
|
||||
points.forEach(function(point){
|
||||
if(point.id === undefined){
|
||||
point.id = this.props.engine.UID();
|
||||
}
|
||||
}.bind(this));
|
||||
var paths = [];
|
||||
if(points.length === 2){
|
||||
paths.push(this.generateLink({
|
||||
onMouseDown: function(event){
|
||||
var point = this.props.engine.getRelativeMousePoint(event);
|
||||
point.id = this.props.engine.UID();
|
||||
this.props.link.points.splice(1,0,point);
|
||||
this.forceUpdate();
|
||||
this.props.newPoint(point.id);
|
||||
}.bind(this),
|
||||
d:
|
||||
" M"+this.getPoint(0).x+" "+this.getPoint(0).y
|
||||
+" C"+(this.getPoint(0).x+50)+" "+this.getPoint(0).y
|
||||
+" " +(this.getPoint(1).x-50)+" "+this.getPoint(1).y
|
||||
+" " +this.getPoint(1).x+" "+this.getPoint(1).y
|
||||
}));
|
||||
if(this.props.link.target === null){
|
||||
paths.push(this.generatePoint(1));
|
||||
}
|
||||
}else{
|
||||
var ds = [];
|
||||
if(this.props.smooth){
|
||||
ds.push(" M"+this.getPoint(0).x+" "+this.getPoint(0).y+" C "+(this.getPoint(0).x+50)+" "+this.getPoint(0).y+" "+this.getPoint(1).x+" "+this.getPoint(1).y+" "+this.getPoint(1).x+" "+this.getPoint(1).y);
|
||||
for(var i = 1;i < points.length-2;i++){
|
||||
ds.push(" M "+this.getPoint(i).x+" "+this.getPoint(i).y+" L "+this.getPoint(i+1).x+" "+this.getPoint(i+1).y);
|
||||
}
|
||||
ds.push(" M"+this.getPoint(i).x+" "+this.getPoint(i).y+" C "+this.getPoint(i).x+" "+this.getPoint(i).y+" "+(this.getPoint(i+1).x-50)+" "+this.getPoint(i+1).y+" "+this.getPoint(i+1).x+" "+this.getPoint(i+1).y);
|
||||
}else{
|
||||
var ds = [];
|
||||
for(var i = 0;i < points.length-1;i++){
|
||||
ds.push(" M "+this.getPoint(i).x+" "+this.getPoint(i).y+" L "+this.getPoint(i+1).x+" "+this.getPoint(i+1).y);
|
||||
}
|
||||
}
|
||||
|
||||
paths = ds.map(function(data,index){
|
||||
return this.generateLink({
|
||||
'data-link':this.props.link.id,
|
||||
'data-point':index,
|
||||
onMouseDown: function(event){
|
||||
var point = this.props.engine.getRelativeMousePoint(event);
|
||||
point.id = this.props.engine.UID();
|
||||
this.props.link.points.splice(index+1,0,point);
|
||||
this.forceUpdate();
|
||||
this.props.newPoint(point.id);
|
||||
}.bind(this),
|
||||
d:data
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
|
||||
//render the circles
|
||||
for(var i = 1;i < points.length-1;i++){
|
||||
paths.push(this.generatePoint(i));
|
||||
}
|
||||
|
||||
if(this.props.link.target === null){
|
||||
paths.push(this.generatePoint(points.length-1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
React.DOM.g(null, paths)
|
||||
);
|
||||
}
|
||||
});
|
||||
32
src/widgets/NodeViewWidget.js
Normal file
32
src/widgets/NodeViewWidget.js
Normal file
@@ -0,0 +1,32 @@
|
||||
var React = require("react");
|
||||
var _ = require("lodash");
|
||||
var Node = require("./NodeWidget");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "NodeViewWidget",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
engine: null
|
||||
};
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.div({className:'node-view'},
|
||||
_.map(this.props.engine.state.nodes,function(node){
|
||||
return(
|
||||
React.createElement(Node,{node: node,engine: this.props.engine},
|
||||
this.props.engine.getNodeFactory(node.type).generateModel(node))
|
||||
);
|
||||
}.bind(this))
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
31
src/widgets/NodeWidget.js
Normal file
31
src/widgets/NodeWidget.js
Normal file
@@ -0,0 +1,31 @@
|
||||
var React = require("react");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "NodeWidget",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
mouseDown: false
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
node: null,
|
||||
engine: null,
|
||||
};
|
||||
},
|
||||
componentDidMount: function(){
|
||||
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.div({
|
||||
'data-nodeid': this.props.node.id,
|
||||
className:'node',
|
||||
style:{top:this.props.node.y+this.props.engine.state.offsetY,left: this.props.node.x+this.props.engine.state.offsetX}},
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
33
src/widgets/PortWidget.js
Normal file
33
src/widgets/PortWidget.js
Normal file
@@ -0,0 +1,33 @@
|
||||
var React = require("react");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "displayName",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
selected: false,
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
name: "unknown",
|
||||
element: null
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.div({
|
||||
onMouseEnter: function(){
|
||||
this.setState({selected: true});
|
||||
}.bind(this),
|
||||
onMouseLeave: function(){
|
||||
this.setState({selected: false});
|
||||
}.bind(this),
|
||||
className:'port'+(this.state.selected?' selected':''),
|
||||
'data-name':this.props.name,
|
||||
'data-nodeid': this.props.node.id
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
43
src/widgets/SVGWidget.js
Normal file
43
src/widgets/SVGWidget.js
Normal file
@@ -0,0 +1,43 @@
|
||||
var React = require("react");
|
||||
var LinkWidget = require("./LinkWidget");
|
||||
var _ = require("lodash");
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "SVG Widget",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
engine: null,
|
||||
newPoint: function(link,pointID){
|
||||
|
||||
}
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.DOM.svg({},
|
||||
_.map(this.props.engine.state.links,function(link){
|
||||
if(link.points.length < 2){
|
||||
return;
|
||||
}else{
|
||||
if(link.source !== null){
|
||||
link.points[0] = this.props.engine.getPortCenter(this.props.engine.getNode(link.source),link.sourcePort);
|
||||
}
|
||||
if(link.target !== null){
|
||||
link.points[link.points.length-1] = this.props.engine.getPortCenter(this.props.engine.getNode(link.target),link.targetPort);
|
||||
}
|
||||
}
|
||||
|
||||
return React.createElement(LinkWidget,{newPoint: function(pointID){
|
||||
this.props.newPoint(link,pointID);
|
||||
}.bind(this),link: link,engine: this.props.engine});
|
||||
}.bind(this))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
13
tests/index.html
Normal file
13
tests/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>STORM Flow Test</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="./bundle.js"></script>
|
||||
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
128
tests/test.js
Normal file
128
tests/test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var Canvas = require("../src/widgets/CanvasWidget");
|
||||
var BasicNodeWidget = require("../src/widgets/BasicNodeWidget");
|
||||
require("./test.scss");
|
||||
|
||||
window.onload = function () {
|
||||
|
||||
var Engine = require("../src/Engine")();
|
||||
|
||||
var Model = {
|
||||
links:[
|
||||
{
|
||||
id: 1,
|
||||
source: 1,
|
||||
sourcePort: 'out',
|
||||
target: 2,
|
||||
targetPort: 'in',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
source: 1,
|
||||
sourcePort: 'out',
|
||||
target: 3,
|
||||
targetPort: 'in'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
source: 2,
|
||||
sourcePort: 'out',
|
||||
target: 4,
|
||||
targetPort: 'in'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
source: 4,
|
||||
sourcePort: 'out',
|
||||
target: 5,
|
||||
targetPort: 'in2'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
source: 2,
|
||||
sourcePort: 'out',
|
||||
target: 5,
|
||||
targetPort: 'in'
|
||||
}
|
||||
],
|
||||
nodes:[
|
||||
{
|
||||
id:1,
|
||||
type: 'action',
|
||||
data: {
|
||||
name: "Create User",
|
||||
outVariables: ['out']
|
||||
},
|
||||
x:50,
|
||||
y:50
|
||||
},
|
||||
{
|
||||
id:2,
|
||||
type: 'action',
|
||||
data: {
|
||||
name: "Add Card to User",
|
||||
inVariables: ['in','in 2'],
|
||||
outVariables: ['out']
|
||||
},
|
||||
x:250,
|
||||
y:50
|
||||
},
|
||||
{
|
||||
id:3,
|
||||
type: 'action',
|
||||
data: {
|
||||
color: 'rgb(0,192,255)',
|
||||
name: "Remove User",
|
||||
inVariables: ['in']
|
||||
},
|
||||
x:250,
|
||||
y:150
|
||||
},
|
||||
{
|
||||
id:4,
|
||||
type: 'action',
|
||||
data: {
|
||||
color: 'rgb(0,192,255)',
|
||||
name: "Remove User",
|
||||
inVariables: ['in'],
|
||||
outVariables: ['out']
|
||||
},
|
||||
x:500,
|
||||
y:150
|
||||
},
|
||||
{
|
||||
id:5,
|
||||
type: 'action',
|
||||
data: {
|
||||
color: 'rgb(192,255,0)',
|
||||
name: "Complex Action 2",
|
||||
inVariables: ['in','in2','in3']
|
||||
},
|
||||
x:800,
|
||||
y:100
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
Engine.registerNodeFactory({
|
||||
type:'action',
|
||||
generateModel: function(model){
|
||||
return React.createElement(BasicNodeWidget,{
|
||||
removeAction: function(){
|
||||
Engine.removeNode(model);
|
||||
},
|
||||
color: model.data.color,
|
||||
node: model,
|
||||
name: model.data.name,
|
||||
inPorts: model.data.inVariables,
|
||||
outPorts: model.data.outVariables
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Engine.loadModel(Model);
|
||||
|
||||
|
||||
ReactDOM.render(React.createElement(Canvas,{engine: Engine}), document.body);
|
||||
};
|
||||
12
tests/test.scss
Normal file
12
tests/test.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
*{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,body{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(60,60,60);
|
||||
/*display: flex;*/
|
||||
}
|
||||
|
||||
@import "../src/sass.scss";
|
||||
24
webpack.config.js
Normal file
24
webpack.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
var webpack = require("webpack");
|
||||
var path = require("path");
|
||||
module.exports = {
|
||||
watch: true,
|
||||
entry: "./tests/test.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "tests"),
|
||||
filename: "bundle.js"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.DedupePlugin()
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loaders: ["style", "css", "sass"]
|
||||
}
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./tests",
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user