commit c37809967ccebddda933d9dad033ddba845ae277 Author: Dylan Vorster Date: Fri Jun 3 11:12:26 2016 +0200 First Import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e303a0f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..d698c69 --- /dev/null +++ b/nbproject/project.properties @@ -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= diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..8a79acf --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.web.clientproject + + + storm-react-flow-2 + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b85be3 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/Engine.js b/src/Engine.js new file mode 100644 index 0000000..e456f5c --- /dev/null +++ b/src/Engine.js @@ -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; + } + }; +}; \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..98f77e0 --- /dev/null +++ b/src/main.js @@ -0,0 +1,3 @@ +module.exports = { + +}; \ No newline at end of file diff --git a/src/sass.scss b/src/sass.scss new file mode 100644 index 0000000..5d6979d --- /dev/null +++ b/src/sass.scss @@ -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; + } + } + } + } +} \ No newline at end of file diff --git a/src/widgets/BasicNodeWidget.js b/src/widgets/BasicNodeWidget.js new file mode 100644 index 0000000..dc94662 --- /dev/null +++ b/src/widgets/BasicNodeWidget.js @@ -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))) + ) + ) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/CanvasWidget.js b/src/widgets/CanvasWidget.js new file mode 100644 index 0000000..35cfca3 --- /dev/null +++ b/src/widgets/CanvasWidget.js @@ -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}) + ) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/LinkWidget.js b/src/widgets/LinkWidget.js new file mode 100644 index 0000000..e98dd8d --- /dev/null +++ b/src/widgets/LinkWidget.js @@ -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) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/NodeViewWidget.js b/src/widgets/NodeViewWidget.js new file mode 100644 index 0000000..caf18e6 --- /dev/null +++ b/src/widgets/NodeViewWidget.js @@ -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)) + + ) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/NodeWidget.js b/src/widgets/NodeWidget.js new file mode 100644 index 0000000..fc938f4 --- /dev/null +++ b/src/widgets/NodeWidget.js @@ -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 + ) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/PortWidget.js b/src/widgets/PortWidget.js new file mode 100644 index 0000000..0a2d94e --- /dev/null +++ b/src/widgets/PortWidget.js @@ -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 + }) + ); + } +}); \ No newline at end of file diff --git a/src/widgets/SVGWidget.js b/src/widgets/SVGWidget.js new file mode 100644 index 0000000..a82fccb --- /dev/null +++ b/src/widgets/SVGWidget.js @@ -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)) + ) + ); + } +}); \ No newline at end of file diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..f6e5c39 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,13 @@ + + + + STORM Flow Test + + + + + + + + + diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 0000000..d09e5c7 --- /dev/null +++ b/tests/test.js @@ -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); +}; diff --git a/tests/test.scss b/tests/test.scss new file mode 100644 index 0000000..d9158a6 --- /dev/null +++ b/tests/test.scss @@ -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"; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..f970508 --- /dev/null +++ b/webpack.config.js @@ -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", + } +};