Compare commits

...

130 Commits

Author SHA1 Message Date
Dylan Vorster
2c730a9fd1 wip 2018-05-05 12:22:02 +02:00
Dylan Vorster
a086bc785e more wip 2018-05-02 11:31:18 +02:00
Dylan Vorster
a7b6012a50 improved generics 2018-05-02 11:23:56 +02:00
Dylan Vorster
4117af8a33 wip 2018-05-02 11:20:23 +02:00
Dylan Vorster
eb7f0642a5 more moving toward react-canvas 2018-05-01 19:23:46 +02:00
Dylan Vorster
a8c73115ac no longer need these actions 2018-05-01 12:15:20 +02:00
Dylan Vorster
404e12c4e8 more stuff we no longer need 2018-04-27 20:42:38 +02:00
Dylan Vorster
aa6d1c336c wip 2018-04-27 20:28:12 +02:00
Dylan Vorster
204e05a2a1 wip 2018-04-27 19:26:18 +02:00
Dylan Vorster
d12220baa0 more refactoring 2018-04-27 16:17:20 +02:00
Dylan Vorster
91c1ee0169 WIP 2018-04-27 15:54:28 +02:00
Dylan Vorster
4806805037 use base zoom and extend from more react-canvas 2018-04-27 15:24:04 +02:00
Dylan Vorster
22f4062f56 Move to react-canvas ;) 2018-04-27 15:08:40 +02:00
Dylan Vorster
32a3c33916 Merge pull request #230 from ganesh-sankey/master
on deserialization of diagram, labels are not getting rendered bug re…
2018-04-23 17:37:37 +02:00
Ganesh Kakade
75ef02dd4d on deserialization of diagram, labels are not getting rendered bug resolved 2018-04-23 20:44:24 +05:30
Dylan Vorster
665c8b3443 Update README.md 2018-04-15 16:36:51 +02:00
Dylan Vorster
ccf425676f Update README.md 2018-04-15 16:36:39 +02:00
Dylan Vorster
1ff3abf3ef Update README.md 2018-04-15 16:36:26 +02:00
Dylan Vorster
0b1dab0de6 Merge pull request #217 from yngndrw/allow-loose-links-fix-and-refactor
Allow loose links fix and refactor (Fix for #171)
2018-04-02 02:28:00 +02:00
Andrew Young
0988e625b1 Added E2E tests, but could not get them to run ? 2018-04-01 23:14:24 +01:00
Andrew Young
dbaf03662f Added @types directory to the .gitignore file 2018-04-01 20:45:19 +01:00
Andrew Young
55f62587bd Re-write the "allowLooseLinks" validation functionality - Now checks both end points on any selected links that have been moved, resulting in more reliable validation 2018-04-01 20:27:55 +01:00
Andrew Young
98bcd60396 Code consistency change 2018-04-01 20:26:41 +01:00
Andrew Young
8467d8e7ca Fixed a typo 2018-04-01 20:26:04 +01:00
Dylan Vorster
40b4e14f15 Merge pull request #207 from wader/always-remove-link-from-old-source-target-port
Always remove link from old source/target port on port change
2018-03-27 17:16:06 +02:00
Mattias Wadman
7a78aaa9fd Always remove link from old source/target port on port change
Was only removed from old if new port is null
2018-03-23 13:43:58 +01:00
Dylan Vorster
47214df76b Update README.md 2018-03-23 10:11:35 +02:00
Dylan Vorster
cc40c398b7 Merge branch 'master' of github.com:projectstorm/react-diagrams 2018-03-21 15:04:02 +02:00
Dylan Vorster
188f63979b 5.1.1 2018-03-21 15:03:05 +02:00
Dylan Vorster
468bcea8b2 Merge pull request #202 from projectstorm/hotfix
Hotfix
2018-03-21 15:03:02 +02:00
Dylan Vorster
54b9feb62e whoops 2018-03-21 14:58:31 +02:00
Dylan Vorster
9f397b1453 fix types directory 2018-03-21 14:55:51 +02:00
Dylan Vorster
445702dd43 Hotfix 2018-03-21 14:52:13 +02:00
Dylan Vorster
bb6a6227e2 Merge pull request #201 from johnetrent/master
Change UglifyJsPlugin's ECMAScript target version to 5
2018-03-21 12:24:09 +02:00
John Trent
afff63e46a Change UglifyJsPlugin's ECMAScript target version to 5 2018-03-20 16:49:18 -07:00
John Trent
03e8348a14 Run prettier on webpack.config.js 2018-03-20 16:45:07 -07:00
Dylan Vorster
2c6d02f101 5.1.0 2018-03-17 14:44:17 +02:00
Dylan Vorster
327dcc190d general improvements 2018-03-17 14:43:35 +02:00
Dylan Vorster
c40c5c5919 improve spacing 2018-03-17 14:30:01 +02:00
Dylan Vorster
388b9931b1 Merge branch '5.1.0' of github.com:projectstorm/react-diagrams into 5.1.0 2018-03-17 14:26:18 +02:00
Dylan Vorster
ae04b5a07f Updates README.md
Auto commit by GitBook Editor
2018-03-17 12:26:03 +00:00
Dylan Vorster
9d2f450440 upgrade images 2018-03-17 14:25:52 +02:00
Dylan Vorster
45d2ea0c70 Merge branch 'master' of github.com:projectstorm/react-diagrams into 5.1.0 2018-03-17 14:16:47 +02:00
Dylan Vorster
94fe0fc854 Updates docs/Getting Started.md
Auto commit by GitBook Editor
2018-03-17 12:16:26 +00:00
Dylan Vorster
adf268697a Deletes test.md
Auto commit by GitBook Editor
2018-03-17 11:08:00 +00:00
Dylan Vorster
2c478db23f Updates test.md
Auto commit by GitBook Editor
2018-03-17 11:01:44 +00:00
Dylan Vorster
3ea6375011 rather import from storm-react-diagrams to further test the library 2018-03-13 22:46:06 +02:00
Dylan Vorster
64ec2dd0a4 Merge pull request #190 from projectstorm/source_map
sourcemap support
2018-03-13 21:42:50 +02:00
Dylan Vorster
e84c2e4e3e fix source map 2018-03-13 21:36:48 +02:00
Dylan Vorster
5f5f13a818 really really fix source maps -_- 2018-03-10 15:52:30 +02:00
Dylan Vorster
2b1a39f236 fix source maps 2018-03-10 15:44:24 +02:00
Dylan Vorster
4402068a93 sourcemap support 2018-03-10 15:32:24 +02:00
Dylan Vorster
ed50438744 Merge pull request #186 from maxleiko/master
fix main.ts exports
2018-03-08 19:25:33 +02:00
maxleiko
9a651c3ecc fix main.ts exports 2018-03-08 18:16:58 +01:00
Dylan Vorster
1af2f8cbe9 Update README.md 2018-03-03 21:57:47 +02:00
Dylan Vorster
2c74a5cf9a Update CHANGELOG.md 2018-03-03 21:57:17 +02:00
Dylan Vorster
140817f8c6 Merge pull request #179 from maxleiko/master
clean refactor / consistent TSLint / Prettier
2018-03-02 19:04:32 +02:00
maxleiko
1debc9c891 clean refactor / consistent TSLint / Prettier 2018-03-02 16:31:50 +01:00
Dylan Vorster
2ee0211994 Merge pull request #173 from wader/basemodel-extends-t
BaseModel extends use T instead of BaseModelListener
2018-02-28 19:55:25 +02:00
Dylan Vorster
86047f69fd Merge pull request #175 from smeijer/feature/add-cross-platform-support
feat(build): add cross-env to support building on various platforms.
2018-02-28 15:59:01 +02:00
Dylan Vorster
036d8dddcf Merge pull request #174 from smeijer/feature/add-vertical-flow-support
feat(link): improve rendering of vertical links
2018-02-28 15:58:18 +02:00
Stephan Meijer
08b81fff56 feat(build): add cross-env to support building on various platforms. 2018-02-28 10:50:30 +01:00
Stephan Meijer
fce1e0c7fe feat(link): improve rendering of vertical links 2018-02-28 10:21:06 +01:00
Mattias Wadman
d69f61e39d BaseModel extends use T instead of BaseModelListener
Use T instead otherwise LinkModelListener will not be used for LinkModel.
Fixes type warning about link.addListener argument.
Probably fixes same issue with iterateListeners.
2018-02-28 00:06:36 +01:00
Dylan Vorster
eb6fac30e0 update readme 2018-02-25 14:12:22 +02:00
Dylan Vorster
446cc8cdff Merge pull request #145 from projectstorm/links_refactor
5.0.0 (WIP)
2018-02-25 14:09:31 +02:00
Dylan Vorster
b8fd5a0690 5.0.0 2018-02-25 14:05:19 +02:00
Dylan Vorster
30195af8ca fix types add changelog 2018-02-25 14:04:59 +02:00
Dylan Vorster
8566c12ccc fix broken paths 2018-02-25 13:47:22 +02:00
Dylan Vorster
ac741d63a6 add some docs 2018-02-25 13:45:57 +02:00
Dylan Vorster
9b4f7d28d6 make jest a bit more defensive 2018-02-25 11:59:14 +02:00
Dylan Vorster
bee07d1be2 worked out a better way to do this 2018-02-25 11:50:24 +02:00
Dylan Vorster
259782ecfa upgrade all the widgets to use bem in a smart way 2018-02-25 11:21:22 +02:00
Dylan Vorster
ec3e022ca3 deserialize labels correctly 2018-02-25 10:16:04 +02:00
Dylan Vorster
60e620539e new snapshots 2018-02-24 19:07:14 +02:00
Dylan Vorster
06510c208c still trying to fix tests 2018-02-24 19:00:58 +02:00
Dylan Vorster
ff8a9c2e40 storybook should use typescript loader 2018-02-24 18:55:24 +02:00
Dylan Vorster
1854130a99 update lock file 2018-02-24 18:49:40 +02:00
Dylan Vorster
e2285cec16 reformat code 2018-02-24 18:44:28 +02:00
Dylan Vorster
070ed985f6 properly handle tests on CI 2018-02-24 18:44:07 +02:00
Dylan Vorster
3f27f99a8f improve e2e tests 2018-02-24 18:41:52 +02:00
Dylan Vorster
23b4a9cb2d add helper methods for E2E tests 2018-02-24 17:44:31 +02:00
Dylan Vorster
fd33ab44c3 remove this 2018-02-24 15:34:37 +02:00
Dylan Vorster
67ac9d1923 vastly improve tests 2018-02-24 15:34:09 +02:00
Dylan Vorster
4d3ad389df more fixes to testing framework 2018-02-24 13:02:17 +02:00
Dylan Vorster
9b3ad7a47f improve testing frameworks 2018-02-24 12:16:47 +02:00
Dylan Vorster
942a0c3894 some demos were using the wrong ports 2018-02-23 19:34:47 +02:00
Dylan Vorster
6980fa091e super advanced custom link demo 2018-02-23 19:29:51 +02:00
Dylan Vorster
a318b71ef1 fix more bugs 2018-02-23 18:58:34 +02:00
Dylan Vorster
6dea182ab8 fix custom links 2018-02-23 18:27:03 +02:00
Dylan Vorster
20675cbf38 prove that multiple labels work 2018-02-23 18:16:43 +02:00
Dylan Vorster
9f78dbba19 forgot to add keys 2018-02-23 18:11:11 +02:00
Dylan Vorster
1b4bbf6493 fix up the labels 2018-02-23 18:10:08 +02:00
Dylan Vorster
317eabb42a add labels to simple example and enable offsets 2018-02-23 17:59:14 +02:00
Dylan Vorster
32fd0000d2 labels work again :D 2018-02-23 17:51:15 +02:00
Dylan Vorster
29ca66989c Update README.md 2018-02-18 20:33:49 +02:00
Dylan Vorster
76fb35f6d1 Fixed some demos, code cleanup 2018-02-18 20:24:52 +02:00
Dylan Vorster
85f97a63fa Merge branch 'smart-routing' of https://github.com/klauspaiva/react-diagrams into links_refactor 2018-02-18 20:13:10 +02:00
Klaus Paiva
f811f12206 Updating docs 2018-02-14 16:08:48 +11:00
Klaus Paiva
183390ab60 Also testing the new story 2018-02-14 15:51:55 +11:00
Klaus Paiva
b243d661c4 Tests for logic in the smart routing class 2018-02-14 10:53:01 +11:00
Klaus Paiva
724b1d3e56 Code re-factoring 2018-02-14 09:34:51 +11:00
Klaus Paiva
088230d100 Correct calculation when nodes are placed at the very edge of the canvas 2018-02-13 07:58:06 +11:00
Klaus Paiva
afa34ae4cf Doing smart routing when conditions are met 2018-02-13 07:42:51 +11:00
Klaus Paiva
4ea968da26 Allowing new connections to be created 2018-02-12 17:18:04 +11:00
Klaus Paiva
2fdf944e0f Smart routing: flattening changes 2018-02-12 16:58:13 +11:00
Dylan Vorster
de40b3841d upgrade node modules, fix stupid mistake lol 2018-02-11 20:28:18 +02:00
Dylan Vorster
7d867dac97 Merge branch 'master' of github.com:projectstorm/react-diagrams into links_refactor 2018-02-11 20:04:27 +02:00
Dylan Vorster
372af4341d Merge branch 'flow-diagram-changes' of https://github.com/yngndrw/react-diagrams into links_refactor 2018-02-11 19:17:14 +02:00
Dylan Vorster
16523dac85 more refactor, this ones a big one 2018-02-11 19:03:08 +02:00
Dylan Vorster
181b2f8122 Merge pull request #146 from maxleiko/master
fixes #139
2018-01-25 21:58:50 +02:00
Dylan Vorster
a365d9eb23 Merge pull request #149 from tomitrescak/patch-1
Impossible to set safely extra porperties
2018-01-25 21:58:02 +02:00
Tomas Trescak
f66c7a6011 Impossible to set safely extra porperties 2018-01-25 19:26:50 +11:00
Andrew Young
6dc3a45851 Added demo to tests and updated test snapshots 2018-01-24 17:16:54 +00:00
Andrew Young
4d6097a1d0 "yarn run pretty" 2018-01-24 17:05:44 +00:00
Andrew Young
b7698ca53a Added a new demo showing off flow diagram usage:
- PortModel can now decide whether or not a link should be allowed (E.g. only allowing outputs to connect to inputs)
- PortModel now has an optional maximum number of links - When set to 1 an existing link is returned by createLinkModel and when set to another finite number null will be returned when the maximum is reached
- LinkModel has been updated to support the resetting of existing links (I.e. removing ports and removing mid-points)
- DiagramWidget has been updated to handle null being returned by createLinkModel as well as an existing link (this also supports an existing link where the link's target port should now be the source port)
- DiagramWidget has been updated to respect the PortModel's new canLinkToPort method
- DiagramWidget has been updated to disallow duplicate links
2018-01-24 17:02:58 +00:00
Maxime Tricoire
e22d9929e5 fixes #139 2018-01-24 14:45:12 +01:00
Dylan Vorster
f1e5a657ba Fixed some stuff that was broken 2018-01-21 16:56:33 +02:00
Dylan Vorster
c447103f0f Quite the overhaul and improvement 2018-01-21 16:29:06 +02:00
Dylan Vorster
c6ac076204 started splitting out the links 2018-01-20 15:41:17 +02:00
Dylan Vorster
266eb85436 Merge pull request #142 from klauspaiva/labels-to-links
Labels to links
2018-01-20 14:11:41 +02:00
Klaus Paiva
070696724c Merge remote-tracking branch 'library/master' into labels-to-links 2018-01-19 09:48:21 +11:00
Klaus Paiva
929790e807 Added new story to tests 2018-01-19 09:44:20 +11:00
Klaus Paiva
54c4be43d3 More code styling 2018-01-19 09:34:53 +11:00
Klaus Paiva
cebd81f000 Executed prettifier task and added missing types 2018-01-19 09:30:27 +11:00
Klaus Paiva
b92641a113 Better demo 2018-01-19 07:56:03 +11:00
Klaus Paiva
72adca100b Smoother animation 2018-01-19 07:33:45 +11:00
Dylan Vorster
455f46ada0 update readme and release notes 2018-01-18 21:33:12 +02:00
Dylan Vorster
8fadbb5d39 update changelog 2018-01-18 20:52:28 +02:00
Klaus Paiva
2c0c5ffaff Rendering a conditional label with static styling 2018-01-18 16:13:15 +11:00
124 changed files with 15952 additions and 13858 deletions

View File

@@ -1,7 +1,3 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build:
@@ -25,6 +21,11 @@ jobs:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
# run tests!
# test building project
- run: yarn run prepublishOnly
# test building storybook
- run: yarn run storybook:build
- run: yarn test
# test e2e tests and jest snapshots
- run: yarn run test:ci

1
.envrc Normal file
View File

@@ -0,0 +1 @@
PATH_add ./node_modules/.bin

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
dist/
dist/main.js
dist/main.js.map
/package
*.tgz
@types/
.out

View File

@@ -5,6 +5,7 @@ docs
.storybook
.circleci
tests
*.md
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node

View File

@@ -1,8 +0,0 @@
import { configure } from '@storybook/react';
function loadStories() {
require('../tests/index.tsx');
// You can require as many demos as you need.
}
configure(loadStories, module);

View File

@@ -50,7 +50,7 @@ export class CodePreview extends React.Component {
<SyntaxHighlighter
customStyle={{width: '100%', overflowX:'hidden', tabSize: 4}}
showLineNumbers={true}
language='typescript'
language='language-tsx'
style={github}
>
{this.state.code}

View File

@@ -22,7 +22,7 @@ module.exports = {
},
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
loader: 'awesome-typescript-loader?declaration=false',
},
{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
@@ -31,6 +31,9 @@ module.exports = {
]
},
resolve: {
alias: {
'storm-react-diagrams': path.join(__dirname, "..", "src", "main")
},
extensions: [".tsx", ".ts", ".js"]
}
};

1
.yarnrc Normal file
View File

@@ -0,0 +1 @@
--ignore-engines true

View File

@@ -1,3 +1,45 @@
__5.1.0__
* [api] Rename XXXFactory into AbstractXXXFactory
* [refactor] tslint and prettier are now the same
* [refactor] Each class now explicitely has its own class file (consistency)
* [feature] Smooth vertical links (no longer limited to horizontal)
* [feature] Dedicated documentation via gitbook
* [bug] forgot to export some
* [refactor] consistently use lodash where possible
* [maintenance] upgrade node modules
__5.0.0__ http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/
PR: https://github.com/projectstorm/react-diagrams/pull/145
* [refactor] Links completely overhauled
* [feature] Smart Routing
* [feature] Flow support
* [demo] Smart Routing
* [demo] Animated links
* [api] Bootstrapping Improvements
* [feature] add custom properties to all widgets
* [refactor] use BEM for all css
* [feature] Default Link factory hooks
* [tests] e2e tests + helper framework
* [tests] automatically load JEST Snapshots
* [feature] Link labels!
__4.0.0__ http://dylanv.blog/2018/01/18/storm-react-diagrams-v4-0-0/
* [refactor] Events system was completely overhauled
* [demo] Custom Link Sizes
* [refactor] Demos are now much more verbose and better managed
* [update] node packages
* [bug] Fix #129
* [feature] Control link creation through ports
* [refactor] Models are now in seperate files
* [refactor] Merged the concept of instance factories and widget factories into one
* [feature] Models can now be cloned at various parts of the model graph
* [demo] Cloning
* [feature] models control isLocked
__3.2.0__ http://dylanv.blog/2017/11/22/storm-react-diagrams-3-2-0/
* [feature] zoom to fit
* added Circle CI tests

View File

@@ -1,89 +1,46 @@
# STORM React Diagrams
__DEMO__: http://www.projectstorm.io/react-diagrams
**PSA**: React Diagrams is currently getting a bit of a rewrite to enable much more advanced features. To see the new foundation WIP visit [https://github.com/projectstorm/react-canvas](https://github.com/projectstorm/react-canvas).
__Latest Release Notes__: http://dylanv.blog/2017/11/22/storm-react-diagrams-3-2-0/
---
**DEMO**: [http://www.projectstorm.io/react-diagrams](http://www.projectstorm.io/react-diagrams)
**DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
**RELEASE NOTES** : [http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/](http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/)
A super simple, no-nonsense diagramming library written in React that just works.
[![Join the chat at https://gitter.im/projectstorm/react-diagrams](https://badges.gitter.im/projectstorm/react-diagrams.svg)](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![NPM](https://img.shields.io/npm/v/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams)
[![NPM](https://img.shields.io/npm/dt/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams)
[![CircleCI](https://circleci.com/gh/projectstorm/react-diagrams/tree/master.svg?style=svg)](https://circleci.com/gh/projectstorm/react-diagrams/tree/master)
[![Join the chat at https://gitter.im/projectstorm/react-diagrams](https://badges.gitter.im/projectstorm/react-diagrams.svg)](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NPM](https://img.shields.io/npm/v/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) [![NPM](https://img.shields.io/npm/dt/storm-react-diagrams.svg)](https://npmjs.org/package/storm-react-diagrams) [![Package Quality](http://npm.packagequality.com/shield/storm-react-diagrams.svg)](http://packagequality.com/#?package=storm-react-diagrams) [![CircleCI](https://circleci.com/gh/projectstorm/react-diagrams/tree/master.svg?style=svg)](https://circleci.com/gh/projectstorm/react-diagrams/tree/master)
![Demo2](./images/example1.png)
![Personal Project](./images/example1.jpg)
![Demo2](./images/example2.png)
![](./images/example2.jpg)
![Demo2](./images/example3.png)
![](./images/example3.jpg)
## Introduction
A no-nonsense diagramming library written entirely in React with the help of Lodash, and a single polyfill. It aims to be:
A no-nonsense diagramming library written entirely in React with the help of a few small libraries. 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..)
* Customizable without having to hack the core \(adapters/factories etc..\)
* Simple to operate and understand without sugar and magic
* Fast and optimized to handle large diagrams with hundreds of nodes/links
* Super easy to use, and should work as you expect it to
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages
## Developer Usage
`npm install storm-react-diagrams` or `yarn add storm-react-diagrams`
### How to run demos
#### Run the demos
After running `yarn install` you must then run: `yarn run storybook`
### How to build
#### Building from source
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 `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.
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.
## [Checkout the docs](https://projectstorm.gitbooks.io/react-diagrams)
### Make your own nodes
To see how to create your own nodes like the one below, take a look at __demo3__:
![Demo2](./images/demo3.png)
## How does it work
The library uses a Model Graph to represent the virtual diagram and then renders the diagram using
2 layers:
* Node Layer -> which is responsible for rendering nodes as HTML components
* Link Layer -> which renders the links as SVG paths
Each node and link is fed into a factory that then generates the corresponding node or link react widget.
Therefore, to create custom nodes and links, register your own factories that return your own widgets.
As long as a node contains at least one port and the corresponding NodeWidget contains at least one PortWidget,
a link can be connected to it.
## Questions
[Questions](docs/Questions.md)
## User Usage
__Delete__ removes any selected items
![__Delete__](./images/rjdDelete.gif)
__Shift + Mouse Drag__ triggers a multi-selection box
![Shift + Mouse Drag](./images/mouseDrag.gif)
__Shift + Mouse Click__ selects the item (items can be multi-selected)
![Shift + Mouse Click](./images/shiftClick.gif)
__Mouse Drag__ drags the entire diagram
![Mouse Drag](./images/canvasDrag.gif)
__Mouse Wheel__ zooms the diagram in / out
![Mouse Wheel](./images/mouseWheel.gif)
__Click Link + Drag__ creates a new link point
![Click Link + Drag](./images/createPoint.gif)
__Click Node Port + Drag__ creates a new link
![Click Node Port + Drag](./images/createLink.gif)

11
SUMMARY.md Normal file
View File

@@ -0,0 +1,11 @@
# Summary
* [Introduction](README.md)
* [Interacting with diagrams](/docs/Interactive Usage.md)
* [Getting Started](/docs/Getting Started.md)
* [About the project](about-the-project.md)
* [Testing](/docs/Testing.md)
* [Architecture Questions](/docs/Architecture Questions.md)

0
about-the-project.md Normal file
View File

View File

@@ -1,83 +1,80 @@
@import "../../src/sass";
@import "../../src/sass/main";
.srd-demo-workspace{
background: black;
display: flex;
flex-direction: column;
height: 100%;
border-radius: 5px;
overflow: hidden;
background: black;
display: flex;
flex-direction: column;
height: 100%;
border-radius: 5px;
overflow: hidden;
&__toolbar{
padding: 5px;
display: flex;
flex-shrink: 0;
&__toolbar{
padding: 5px;
display: flex;
flex-shrink: 0;
button{
background: rgb(60,60,60);
font-size: 14px;
padding: 5px 10px;
border: none;
color: white;
outline: none;
cursor: pointer;
margin: 2px;
border-radius: 3px;
button{
background: rgb(60,60,60);
font-size: 14px;
padding: 5px 10px;
border: none;
color: white;
outline: none;
cursor: pointer;
margin: 2px;
border-radius: 3px;
&:hover{
background: rgb(0,192,255);
}
}
}
&:hover{
background: rgb(0,192,255);
}
}
}
&__content{
flex-grow: 1;
height: 100%;
}
&__content{
flex-grow: 1;
height: 100%;
}
}
.docs-preview-wrapper{
background: rgb(60,60,60);
border-radius: 10px;
overflow: hidden;
padding: 10px;
margin-top: 20px;
margin-bottom: 20px;
background: rgb(60,60,60);
border-radius: 10px;
overflow: hidden;
padding: 10px;
margin-top: 20px;
margin-bottom: 20px;
}
.storm-diagrams-canvas{
height: 100%;
min-height: 300px;
background-color: rgb(60,60,60) !important;
$color: rgba(white, .05);
background-image:
linear-gradient(0deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent),
linear-gradient(90deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent);
background-size:50px 50px;
.srd-demo-canvas{
height: 100%;
min-height: 300px;
background-color: rgb(60,60,60) !important;
$color: rgba(white, .05);
background-image:
linear-gradient(0deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent),
linear-gradient(90deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent);
background-size:50px 50px;
path{
stroke: rgba(white,0.5);
}
.pointui{
fill: rgba(white,0.5);
}
.pointui{
fill: rgba(white,0.5);
}
}

View File

@@ -1,25 +1,21 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
NodeModel,
DefaultPortModel,
DiagramWidget,
BaseModel
} from "../../src/main";
} from "storm-react-diagrams";
import * as _ from "lodash";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
import { DefaultPortFactory } from "../../src/defaults/DefaultPortFactory";
/**
* Tests cloning
*/
class CloneSelected extends React.Component<any, any> {
constructor(props) {
constructor(props: any) {
super(props);
this.cloneSelected = this.cloneSelected.bind(this);
}
@@ -28,7 +24,6 @@ class CloneSelected extends React.Component<any, any> {
let { engine } = this.props;
let offset = { x: 100, y: 100 };
let model = engine.getDiagramModel();
let originalItems = model.getSelectedItems("link", "node");
let itemMap = {};
_.forEach(model.getSelectedItems(), (item: BaseModel<any>) => {
@@ -55,7 +50,7 @@ class CloneSelected extends React.Component<any, any> {
const { engine } = this.props;
return (
<DemoWorkspaceWidget buttons={<button onClick={this.cloneSelected}>Clone Selected</button>}>
<DiagramWidget diagramEngine={engine} />
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
}
@@ -64,35 +59,29 @@ class CloneSelected extends React.Component<any, any> {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
let port = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
// link the ports
let link1 = port.link(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <CloneSelected engine={engine} model={model} />;

View File

@@ -1,7 +1,5 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
@@ -9,51 +7,106 @@ import {
DiagramWidget,
LinkWidget,
LinkProps,
DefaultLinkWidget
} from "../../src/main";
DefaultLinkWidget,
DefaultLinkModel,
DefaultLinkFactory
} from "storm-react-diagrams";
import { action } from "@storybook/addon-actions";
import * as React from "react";
import { LinkFactory } from "../../src/AbstractFactory";
export class AdvancedLinkModel extends LinkModel {
size: number;
color: string;
export class AdvancedLinkModel extends DefaultLinkModel {
constructor() {
super("advanced");
this.color = "green";
this.size = 6;
this.width = 10;
}
}
export class AdvancedPortModel extends DefaultPortModel {
constructor(isInput: boolean, name: string, label: string = null, id?: string) {
super(isInput, name, label, id);
}
createLinkModel(): LinkModel | null {
var link = new AdvancedLinkModel();
link.setSourcePort(this);
return link;
createLinkModel(): AdvancedLinkModel | null {
return new AdvancedLinkModel();
}
}
export class AdvancedLinkWidgetFactory extends LinkFactory<AdvancedLinkModel> {
export class AdvancedLinkSegment extends React.Component<{ model: AdvancedLinkModel; path: string }> {
path: SVGPathElement;
circle: SVGCircleElement;
callback: () => any;
percent: number;
handle: any;
mounted: boolean;
constructor(props) {
super(props);
this.percent = 0;
}
componentDidMount() {
this.mounted = true;
this.callback = () => {
if (!this.circle || !this.path) {
return;
}
this.percent += 2;
if (this.percent > 100) {
this.percent = 0;
}
let point = this.path.getPointAtLength(this.path.getTotalLength() * (this.percent / 100.0));
this.circle.setAttribute("cx", "" + point.x);
this.circle.setAttribute("cy", "" + point.y);
if (this.mounted) {
requestAnimationFrame(this.callback);
}
};
requestAnimationFrame(this.callback);
}
componentWillUnmount() {
this.mounted = false;
}
render() {
return (
<>
<path
ref={ref => {
this.path = ref;
}}
strokeWidth={this.props.model.width}
stroke="rgba(255,0,0,0.5)"
d={this.props.path}
/>
<circle
ref={ref => {
this.circle = ref;
}}
r={10}
fill="orange"
/>
</>
);
}
}
export class AdvancedLinkFactory extends DefaultLinkFactory {
constructor() {
super();
this.type = "advanced";
}
getNewInstance(initialConfig?: any): AdvancedLinkModel {
return new AdvancedLinkModel();
}
constructor() {
super("advanced");
}
generateReactWidget(diagramEngine: DiagramEngine, link: AdvancedLinkModel): JSX.Element {
return React.createElement(DefaultLinkWidget, {
color: link.color,
width: link.size,
link,
diagramEngine
});
generateLinkSegment(model: AdvancedLinkModel, widget: DefaultLinkWidget, selected: boolean, path: string) {
return (
<g>
<AdvancedLinkSegment model={model} path={path} />
</g>
);
}
}
/**
@@ -65,50 +118,40 @@ export class AdvancedLinkWidgetFactory extends LinkFactory<AdvancedLinkModel> {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.registerLinkFactory(new AdvancedLinkWidgetFactory());
engine.installDefaults();
engine.registerLinkFactory(new AdvancedLinkFactory());
// create some nodes
var node1 = new DefaultNodeModel("Source", "rgb(0,192,255)");
var port1 = node1.addPort(new AdvancedPortModel(false, "out-1", "Out thick"));
var port2 = node1.addPort(new DefaultPortModel(false, "out-2", "Out default"));
node1.x = 100;
node1.y = 100;
let port1 = node1.addPort(new AdvancedPortModel(false, "out-1", "Out thick"));
let port2 = node1.addPort(new DefaultPortModel(false, "out-2", "Out default"));
node1.setPosition(100, 100);
var node2 = new DefaultNodeModel("Target", "rgb(192,255,0)");
var port3 = node2.addPort(new AdvancedPortModel(true, "in-1", "In thick"));
var port4 = node2.addPort(new DefaultPortModel(true, "in-2", "In default"));
node2.x = 300;
node2.y = 100;
node2.setPosition(300, 100);
var node3 = new DefaultNodeModel("Source", "rgb(0,192,255)");
var port5 = node3.addPort(new AdvancedPortModel(false, "out-1", "Out thick"));
var port6 = node3.addPort(new DefaultPortModel(false, "out-2", "Out default"));
node3.x = 100;
node3.y = 200;
node3.addPort(new AdvancedPortModel(false, "out-1", "Out thick"));
node3.addPort(new DefaultPortModel(false, "out-2", "Out default"));
node3.setPosition(100, 200);
var node4 = new DefaultNodeModel("Target", "rgb(192,255,0)");
var port7 = node4.addPort(new AdvancedPortModel(true, "in-1", "In thick"));
var port8 = node4.addPort(new DefaultPortModel(true, "in-2", "In default"));
node4.x = 300;
node4.y = 200;
node4.addPort(new AdvancedPortModel(true, "in-1", "In thick"));
node4.addPort(new DefaultPortModel(true, "in-2", "In default"));
node4.setPosition(300, 200);
var model = new DiagramModel();
model.addNode(node1);
model.addNode(node2);
model.addNode(node3);
model.addNode(node4);
model.addAll(port1.link(port3), port2.link(port4));
var link1 = node1.getOutPorts()[0].createLinkModel();
link1.setTargetPort(port3);
model.addLink(link1);
var link2 = node1.getOutPorts()[1].createLinkModel();
link2.setTargetPort(port4);
model.addLink(link2);
// add everything else
model.addAll(node1, node2, node3, node4);
// load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
// render the diagram!
return <DiagramWidget diagramEngine={engine} />;
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};

View File

@@ -1,14 +1,15 @@
import * as SRD from "../../src/main";
import { DiamonNodeWidgetFactory } from "./DiamondNodeWidget";
import * as SRD from "storm-react-diagrams";
import { DiamonNodeWidget } from "./DiamondNodeWidget";
import { DiamondNodeModel } from "./DiamondNodeModel";
import * as React from "react";
export class DiamondNodeFactory extends SRD.NodeFactory {
export class DiamondNodeFactory extends SRD.AbstractNodeFactory {
constructor() {
super("diamond");
}
generateReactWidget(diagramEngine: SRD.DiagramEngine, node: SRD.NodeModel): JSX.Element {
return DiamonNodeWidgetFactory({ node: node });
return <DiamonNodeWidget node={node} />;
}
getNewInstance() {

View File

@@ -1,7 +1,7 @@
import * as SRD from "../../src/main";
import { NodeModel } from "storm-react-diagrams";
import { DiamondPortModel } from "./DiamondPortModel";
export class DiamondNodeModel extends SRD.NodeModel {
export class DiamondNodeModel extends NodeModel {
constructor() {
super("diamond");
this.addPort(new DiamondPortModel("top"));

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { DiamondNodeModel } from "./DiamondNodeModel";
import { PortWidget } from "../../src/main";
import { PortWidget } from "storm-react-diagrams";
export interface DiamonNodeWidgetProps {
node: DiamondNodeModel;
@@ -39,10 +39,10 @@ export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, Dia
dangerouslySetInnerHTML={{
__html:
`
<g id="Layer_1">
</g>
<g id="Layer_2">
<polygon fill="purple" stroke="#000000" stroke-width="3" stroke-miterlimit="10" points="10,` +
<g id="Layer_1">
</g>
<g id="Layer_2">
<polygon fill="purple" stroke="#000000" stroke-width="3" stroke-miterlimit="10" points="10,` +
this.props.size / 2 +
` ` +
this.props.size / 2 +
@@ -55,8 +55,8 @@ export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, Dia
`,` +
(this.props.size - 10) +
` "/>
</g>
`
</g>
`
}}
/>
<div
@@ -103,5 +103,3 @@ export class DiamonNodeWidget extends React.Component<DiamonNodeWidgetProps, Dia
);
}
}
export var DiamonNodeWidgetFactory = React.createFactory(DiamonNodeWidget);

View File

@@ -1,11 +1,11 @@
import * as SRD from "../../src/main";
import * as _ from "lodash";
import { LinkModel, DiagramEngine, PortModel, DefaultLinkModel } from "storm-react-diagrams";
export class DiamondPortModel extends SRD.PortModel {
export class DiamondPortModel extends PortModel {
position: string | "top" | "bottom" | "left" | "right";
constructor(pos: string = "top") {
super(pos);
super(pos, "diamond");
this.position = pos;
}
@@ -15,8 +15,12 @@ export class DiamondPortModel extends SRD.PortModel {
});
}
deSerialize(data: any) {
super.deSerialize(data);
deSerialize(data: any, engine: DiagramEngine) {
super.deSerialize(data, engine);
this.position = data.position;
}
createLinkModel(): LinkModel {
return new DefaultLinkModel();
}
}

View File

@@ -0,0 +1,14 @@
import { PortModel, AbstractPortFactory } from "storm-react-diagrams";
export class SimplePortFactory extends AbstractPortFactory {
cb: (initialConfig?: any) => PortModel;
constructor(type: string, cb: (initialConfig?: any) => PortModel) {
super(type);
this.cb = cb;
}
getNewInstance(initialConfig?: any): PortModel {
return this.cb(initialConfig);
}
}

View File

@@ -1,17 +1,17 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
} from "storm-react-diagrams";
import * as React from "react";
// import the custom models
import { DiamondNodeModel } from "./DiamondNodeModel";
import { DiamondNodeFactory } from "./DiamondNodeFactory";
import { SimplePortFactory } from "../../src/AbstractFactory";
import { SimplePortFactory } from "./SimplePortFactory";
import { DiamondPortModel } from "./DiamondPortModel";
/**
@@ -20,7 +20,9 @@ import { DiamondPortModel } from "./DiamondPortModel";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
// register some other factories as well
engine.registerPortFactory(new SimplePortFactory("diamond", config => new DiamondPortModel()));
engine.registerNodeFactory(new DiamondNodeFactory());
@@ -29,62 +31,27 @@ export default () => {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 150;
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 150);
//3-B) create our new custom node
var node2 = new DiamondNodeModel();
node2.x = 400;
node2.y = 100;
node2.setPosition(250, 108);
var node3 = new DefaultNodeModel("Node 3", "red");
var port3 = node3.addPort(new DefaultPortModel(true, "in-1", "In"));
node3.x = 800;
node3.y = 150;
var port3 = node3.addInPort("In");
node3.setPosition(500, 150);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(node2.ports["left"]);
var link2 = new LinkModel();
link2.setSourcePort(node2.ports["right"]);
link2.setTargetPort(port3);
var link1 = port1.link(node2.getPort("left"));
var link2 = port3.link(node2.getPort("right"));
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addNode(node3);
model.addLink(link1);
model.addLink(link2);
model.addAll(node1, node2, node3, link1, link2);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget diagramEngine={engine} />;
// //!------------- SERIALIZING / DESERIALIZING ------------
//
// //we need this to help the system know what models to create form the JSON
// engine.registerInstanceFactory(new SRD.DefaultNodeInstanceFactory());
// engine.registerInstanceFactory(new SRD.DefaultPortInstanceFactory());
// engine.registerInstanceFactory(new SRD.LinkInstanceFactory());
// engine.registerInstanceFactory(new DiamondNodeFactory());
// engine.registerInstanceFactory(new DiamondPortFactory());
//
// //serialize the model
// var str = JSON.stringify(model.serializeDiagram());
// console.log(str);
//
// //deserialize the model
// var model2 = new SRD.DiagramModel();
// model2.deSerializeDiagram(JSON.parse(str),engine);
// engine.setDiagramModel(model2);
// console.log(model2);
//
// //re-render the model
// ReactDOM.render(React.createElement(SRD.DiagramWidget,{diagramEngine: engine}), document.body);
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};

View File

@@ -22,9 +22,7 @@ function distributeGraph(model) {
let edges = mapEdges(model);
let graph = new dagre.graphlib.Graph();
graph.setGraph({});
graph.setDefaultEdgeLabel(function() {
return {};
});
graph.setDefaultEdgeLabel(() => ({}));
//add elements to dagre graph
nodes.forEach(node => {
graph.setNode(node.id, node.metadata);

View File

@@ -7,7 +7,7 @@ import {
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
} from "storm-react-diagrams";
import { distributeElements } from "./dagre-utils";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
@@ -23,10 +23,7 @@ function connectNodes(nodeFrom, nodeTo) {
count++;
const portOut = nodeFrom.addPort(new DefaultPortModel(true, `${nodeFrom.name}-out-${count}`, "Out"));
const portTo = nodeTo.addPort(new DefaultPortModel(false, `${nodeFrom.name}-to-${count}`, "IN"));
let link = new LinkModel();
link.setSourcePort(portOut);
link.setTargetPort(portTo);
return link;
return portOut.link(portTo);
}
/**
@@ -43,7 +40,7 @@ class Demo8Widget extends React.Component<any, any> {
const { engine } = this.props;
const model = engine.getDiagramModel();
let distributedModel = getDistributedModel(engine, model);
engine.setDiagramModel(distributedModel);
engine.setModel(distributedModel);
this.forceUpdate();
}
@@ -52,7 +49,7 @@ class Demo8Widget extends React.Component<any, any> {
return (
<DemoWorkspaceWidget buttons={<button onClick={this.autoDistribute}>Re-distribute</button>}>
<DiagramWidget diagramEngine={engine} />
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
}
@@ -61,6 +58,7 @@ class Demo8Widget extends React.Component<any, any> {
function getDistributedModel(engine, model) {
const serialized = model.serializeDiagram();
const distributedSerializedDiagram = distributeElements(serialized);
//deserialize the model
let deSerializedModel = new DiagramModel();
deSerializedModel.deSerializeDiagram(distributedSerializedDiagram, engine);
@@ -70,13 +68,12 @@ function getDistributedModel(engine, model) {
export default () => {
//1) setup the diagram engine
let engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
let model = new DiagramModel();
//3) create a default nodes
let nodesFrom = [];
let nodesTo = [];
@@ -87,30 +84,37 @@ export default () => {
nodesTo.push(createNode("to-1"));
nodesTo.push(createNode("to-2"));
nodesTo.push(createNode("to-3"));
//4) link nodes together
let links = nodesFrom.map((node, index) => {
return connectNodes(node, nodesTo[index]);
});
// more links for more complicated diagram
links.push(connectNodes(nodesFrom[0], nodesTo[1]));
links.push(connectNodes(nodesTo[0], nodesFrom[1]));
links.push(connectNodes(nodesFrom[1], nodesTo[2]));
// initial random position
nodesFrom.forEach((node, index) => {
node.x = index * 70;
model.addNode(node);
});
nodesTo.forEach((node, index) => {
node.x = index * 70;
node.y = 100;
model.addNode(node);
});
links.forEach(link => {
model.addLink(link);
});
//5) load model into engine
let model2 = getDistributedModel(engine, model);
engine.setDiagramModel(model2);
engine.setModel(model2);
return <Demo8Widget engine={engine} />;
};

View File

@@ -1,4 +1,5 @@
import * as SRD from "../../src/main";
import * as SRD from "storm-react-diagrams";
/**
* @author Dylan Vorster
*/
@@ -8,32 +9,29 @@ export class Application {
constructor() {
this.diagramEngine = new SRD.DiagramEngine();
this.diagramEngine.installDefaultFactories();
this.diagramEngine.installDefaults();
this.newModel();
}
public newModel() {
this.activeModel = new SRD.DiagramModel();
this.diagramEngine.setDiagramModel(this.activeModel);
this.diagramEngine.setModel(this.activeModel);
//3-A) create a default node
var node1 = new SRD.DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new SRD.DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
let port = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new SRD.DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new SRD.DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
var link1 = new SRD.LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
// link the ports
let link1 = port.link(port2);
this.activeModel.addNode(node1);
this.activeModel.addNode(node2);
this.activeModel.addLink(link1);
this.activeModel.addAll(node1, node2, link1);
}
public getActiveDiagram(): SRD.DiagramModel {

View File

@@ -1,10 +1,9 @@
import * as React from "react";
import * as _ from "lodash";
import { TrayWidget } from "./TrayWidget";
import { DiagramWidget } from "../../../src/main";
import { Application } from "../Application";
import { TrayItemWidget } from "./TrayItemWidget";
import { DefaultNodeModel, DefaultPortModel } from "../../../src/main";
import { DefaultNodeModel, DiagramWidget } from "storm-react-diagrams";
export interface BodyWidgetProps {
app: Application;
@@ -46,10 +45,10 @@ export class BodyWidget extends React.Component<BodyWidgetProps, BodyWidgetState
var node = null;
if (data.type === "in") {
node = new DefaultNodeModel("Node " + (nodesCount + 1), "rgb(192,255,0)");
node.addPort(new DefaultPortModel(true, "in-1", "In"));
node.addInPort("In");
} else {
node = new DefaultNodeModel("Node " + (nodesCount + 1), "rgb(0,192,255)");
node.addPort(new DefaultPortModel(false, "out-1", "Out"));
node.addOutPort("Out");
}
var points = this.props.app.getDiagramEngine().getRelativeMousePoint(event);
node.x = points.x;
@@ -64,7 +63,7 @@ export class BodyWidget extends React.Component<BodyWidgetProps, BodyWidgetState
event.preventDefault();
}}
>
<DiagramWidget diagramEngine={this.props.app.getDiagramEngine()} />
<DiagramWidget className="srd-demo-canvas" diagramEngine={this.props.app.getDiagramEngine()} />
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@ import * as React from "react";
import { BodyWidget } from "./components/BodyWidget";
import { Application } from "./Application";
require("./sass/main.scss");
import "./sass/main.scss";
export default () => {
var app = new Application();

View File

@@ -1,82 +1,47 @@
.body{
flex-grow: 1;
display: flex;
flex-direction: column;
min-height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
min-height: 100%;
.header{
display: flex;
background: rgb(30,30,30);
flex-grow: 0;
flex-shrink: 0;
color: white;
font-family: Helvetica, Arial;
padding: 10px;
>*{
align-self:center;
}
}
.header{
display: flex;
background: rgb(30,30,30);
flex-grow: 0;
flex-shrink: 0;
color: white;
font-family: Helvetica, Arial;
padding: 10px;
>*{
align-self:center;
}
}
.content{
display: flex;
flex-grow: 1;
.content{
display: flex;
flex-grow: 1;
.diagram-layer{
position: relative;
flex-grow: 1;
}
.diagram-layer{
position: relative;
flex-grow: 1;
}
.tray{
min-width: 200px;
background: rgb(20,20,20);
flex-grow: 0;
flex-shrink: 0;
.tray{
min-width: 200px;
background: rgb(20,20,20);
flex-grow: 0;
flex-shrink: 0;
.tray-item{
color: white;
font-family: Helvetica, Arial;
padding: 5px;
margin: 0px 10px;
border: solid 1px;
border-radius: 5px;
margin-bottom: 2px;
cursor: pointer;
}
}
}
}
.storm-diagrams-canvas{
$color: rgba(white, .05);
background: rgba(black,0.2);
background-image:
linear-gradient(0deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent),
linear-gradient(90deg,
transparent 24%,
$color 25%,
$color 26%,
transparent 27%,
transparent 74%,
$color 75%,
$color 76%,
transparent 77%,
transparent);
background-size:50px 50px;
path{
stroke: rgba(white,0.5);
}
.pointui{
fill: rgba(white,0.5);
}
.tray-item{
color: white;
font-family: Helvetica, Arial;
padding: 5px;
margin: 0px 10px;
border: solid 1px;
border-radius: 5px;
margin-bottom: 2px;
cursor: pointer;
}
}
}
}

View File

@@ -1,13 +1,4 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
import * as React from "react";
/**
@@ -16,7 +7,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -24,29 +15,23 @@ export default () => {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
let port = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
// link the ports
let link1 = port.link(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget diagramEngine={engine} />;
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};

View File

@@ -0,0 +1,73 @@
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget,
DefaultLinkModel
} from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
import { action } from "@storybook/addon-actions";
export default () => {
// setup the diagram engine
const engine = new DiagramEngine();
engine.installDefaults();
// setup the diagram model
const model = new DiagramModel();
// create four nodes
const node1 = new DefaultNodeModel("Node A", "rgb(0,192,255)");
const port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
const node2 = new DefaultNodeModel("Node B", "rgb(255,255,0)");
const port2 = node2.addInPort("In");
node2.setPosition(400, 50);
const node3 = new DefaultNodeModel("Node C (no label)", "rgb(192,255,255)");
const port3 = node3.addInPort("In");
node3.setPosition(450, 180);
const node4 = new DefaultNodeModel("Node D", "rgb(192,0,255)");
const port4 = node4.addInPort("In");
node4.setPosition(300, 250);
// link node A and B together and give it a label
const link1 = port1.link(port2);
(link1 as DefaultLinkModel).addLabel("Custom label 1");
(link1 as DefaultLinkModel).addLabel("Custom label 2");
// no label for A and C, just a link
const link2 = port1.link(port3);
// also a label for A and D
const link3 = port1.link(port4);
link3.setTargetPort(port4);
(link3 as DefaultLinkModel).addLabel("Emoji label: 🎉");
// add all to the main model
model.addAll(node1, node2, node3, node4, link1, link2, link3);
// load model into engine and render
engine.setModel(model);
return (
<DemoWorkspaceWidget
buttons={
<button
onClick={() => {
action("Serialized Graph")(JSON.stringify(model.serializeDiagram(), null, 2));
}}
>
Serialize Graph
</button>
}
>
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
};

View File

@@ -1,6 +1,12 @@
import * as SRD from "../../src/main";
import * as React from "react";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "../../src/main";
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DiagramWidget,
DiagramProps
} from "storm-react-diagrams";
/**
* Shows that a limit of points can be set for links
@@ -8,40 +14,36 @@ import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget
export default () => {
// setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
// sample for link with simple line (no additional points created by default)
var node1 = new DefaultNodeModel("Node 1", "rgb(255,99,66)");
var port1 = node1.addPort(new SRD.DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
let port = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new SRD.DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
// link the ports
let link1 = port.link(port2);
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
engine.setDiagramModel(model);
engine.setModel(model);
var props = {
diagramEngine: engine,
maxNumberPointsPerLink: 5
} as SRD.DiagramProps;
} as DiagramProps;
return (
<div>
<p>A maximum of 5 points can be created per link.</p>
<DiagramWidget {...props} />
<DiagramWidget className="srd-demo-canvas" {...props} />
</div>
);
};

View File

@@ -1,7 +1,13 @@
import * as SRD from "../../src/main";
import * as React from "react";
import { action } from "@storybook/addon-actions";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "../../src/main";
import {
DiagramEngine,
DiagramModel,
DiagramProps,
DefaultNodeModel,
LinkModel,
DiagramWidget
} from "storm-react-diagrams";
/**
* Shows some of the events triggered when elements are selected
@@ -9,56 +15,48 @@ import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget
export default () => {
// setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
// sample for link with simple line
var node1 = new DefaultNodeModel("Node 1", "rgb(255,99,66)");
var port1 = node1.addPort(new SRD.DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new SRD.DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 40;
var port2 = node2.addInPort("In");
node2.setPosition(400, 40);
var node3 = new DefaultNodeModel("Node 3", "rgb(128,99,255)");
var port3 = node3.addPort(new SRD.DefaultPortModel(true, "in-2", "IN"));
node3.x = 300;
node3.y = 160;
var port3 = node3.addInPort("In");
node3.setPosition(300, 160);
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
var link2 = new LinkModel();
link2.setSourcePort(port1);
link2.setTargetPort(port3);
//link the nodes
let link1 = port1.link(port2);
let link2 = port1.link(port3);
model.addNode(node1);
model.addNode(node2);
model.addNode(node3);
model.addLink(link1);
model.addLink(link2);
// add all the models
let models = model.addAll(node1, node2, node3, link1, link2);
[node1, node2, link1, link2].forEach(item => {
// add a selection listener to each
models.forEach(item => {
item.addListener({
selectionChanged: action("selectionChanged")
});
});
engine.setDiagramModel(model);
engine.setModel(model);
var props = {
diagramEngine: engine,
maxNumberPointsPerLink: 0 // no extra points so link selection is fired straight away
} as SRD.DiagramProps;
} as DiagramProps;
return (
<div>
<p>Click the diagram elements to inspect some of the possible events.</p>
<DiagramWidget {...props} />
<DiagramWidget className="srd-demo-canvas" {...props} />
</div>
);
};

View File

@@ -1,6 +1,13 @@
import * as SRD from "../../src/main";
import * as React from "react";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, PointModel, DiagramWidget } from "../../src/main";
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
PointModel,
DiagramWidget,
DiagramProps
} from "storm-react-diagrams";
/**
*
@@ -11,54 +18,40 @@ import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, PointModel, D
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
var model = new DiagramModel();
// sample for link with simple line (no additional points)
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new SRD.DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new SRD.DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
var port2 = node2.addInPort("In");
node2.setPosition(400, 100);
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
let link1 = port1.link(port2);
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
// sample for link with complex line (additional points)
var node3 = new DefaultNodeModel("Node 3", "rgb(0,192,255)");
var port3 = node3.addPort(new SRD.DefaultPortModel(false, "out-2", "Out"));
node3.x = 100;
node3.y = 250;
var port3 = node3.addOutPort("Out");
node3.setPosition(100, 250);
var node4 = new DefaultNodeModel("Node 4", "rgb(192,255,0)");
var port4 = node4.addPort(new SRD.DefaultPortModel(true, "in-2", "IN"));
node4.x = 400;
node4.y = 250;
var port4 = node4.addInPort("In");
node4.setPosition(400, 250);
var link2 = new LinkModel();
link2.setSourcePort(port3);
link2.setTargetPort(port4);
var link2 = port3.link(port4);
var additionalPoint1 = new PointModel(link2, { x: 350, y: 225 });
link2.addPoint(additionalPoint1);
var additionalPoint2 = new PointModel(link2, { x: 200, y: 225 });
link2.addPoint(additionalPoint2);
link2.point(350, 225);
link2.point(200, 225);
model.addNode(node3);
model.addNode(node4);
model.addLink(link2);
model.addAll(node3, node4, link2);
engine.setDiagramModel(model);
engine.setModel(model);
//!========================================= <<<<<<<
@@ -68,9 +61,9 @@ export default () => {
allowLooseLinks: false,
allowCanvasTranslation: false,
allowCanvasZoom: false
} as SRD.DiagramProps;
} as DiagramProps;
//!========================================= <<<<<<<
return <DiagramWidget {...props} />;
return <DiagramWidget className="srd-demo-canvas" {...props} />;
};

View File

@@ -1,13 +1,4 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
@@ -40,7 +31,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
node.x += 30;
node.y += 30;
model2.deSerializeDiagram(obj, engine);
engine.setDiagramModel(model2);
engine.setModel(model2);
this.forceUpdate();
}
@@ -57,7 +48,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
</button>
]}
>
<DiagramWidget diagramEngine={engine} />
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
}
@@ -66,35 +57,29 @@ class NodeDelayedPosition extends React.Component<any, any> {
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
var port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
var link1 = port1.link(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <NodeDelayedPosition engine={engine} model={model} />;

View File

@@ -1,11 +1,4 @@
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
import * as React from "react";
/**
@@ -18,31 +11,7 @@ import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100 + offsetX;
node1.y = 100 + offsetY;
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 200 + offsetX;
node2.y = 100 + offsetY;
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
}
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -54,8 +23,26 @@ export default () => {
}
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget diagramEngine={engine} />;
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};
function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addOutPort("Out");
node1.setPosition(100 + offsetX, 100 + offsetY);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addInPort("In");
node2.setPosition(200 + offsetX, 100 + offsetY);
//3-C) link the 2 nodes together
var link1 = port1.link(port2);
//4) add the models to the root graph
model.addAll(node1, node2, link1);
}

View File

@@ -1,60 +1,46 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel } from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
import { action } from "@storybook/addon-actions";
var beautify = require("json-beautify");
import * as beautify from "json-beautify";
import {DeserializeEvent} from "../../../react-canvas/src/models/BaseModel";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
var port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
var link1 = port1.link(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//!------------- SERIALIZING ------------------
var str = JSON.stringify(model.serializeDiagram());
var str = JSON.stringify(model.serialize());
//!------------- DESERIALIZING ----------------
var model2 = new DiagramModel();
model2.deSerializeDiagram(JSON.parse(str), engine);
engine.setDiagramModel(model2);
model2.deSerialize(new DeserializeEvent(JSON.parse(str), engine));
engine.setModel(model2);
return (
<DemoWorkspaceWidget
@@ -68,7 +54,7 @@ export default () => {
</button>
}
>
<DiagramWidget diagramEngine={engine} />
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
};

View File

@@ -0,0 +1,38 @@
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = port1.link(port2);
//3-D) create an orphaned node
var node3 = new DefaultNodeModel("Node 3", "rgb(0,192,255)");
node3.addOutPort("Out");
node3.setPosition(100, 200);
//4) add the models to the root graph
model.addAll(node1, node2, node3, link1);
//5) load model into engine
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} allowLooseLinks={false} />;
};

View File

@@ -3,44 +3,39 @@ import {
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
DiagramWidget,
DefaultLinkModel
} from "storm-react-diagrams";
import * as React from "react";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100;
node1.y = 100;
let port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 400;
node2.y = 100;
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
// link the ports
let link1 = port1.link(port2);
(link1 as DefaultLinkModel).addLabel("Hello World!");
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
model.addAll(node1, node2, link1);
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return <DiagramWidget diagramEngine={engine} />;
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};

View File

@@ -0,0 +1,68 @@
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
import { action } from "@storybook/addon-actions";
export default () => {
// setup the diagram engine
const engine = new DiagramEngine();
engine.installDefaults();
// setup the diagram model
const model = new DiagramModel();
// create four nodes in a way that straight links wouldn't work
const node1 = new DefaultNodeModel("Node A", "rgb(0,192,255)");
const port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.setPosition(340, 350);
const node2 = new DefaultNodeModel("Node B", "rgb(255,255,0)");
const port2 = node2.addPort(new DefaultPortModel(false, "out-1", "Out"));
node2.setPosition(240, 80);
const node3 = new DefaultNodeModel("Node C", "rgb(192,255,255)");
const port3 = node3.addPort(new DefaultPortModel(true, "in-1", "In"));
node3.setPosition(540, 180);
const node4 = new DefaultNodeModel("Node D", "rgb(192,0,255)");
const port4 = node4.addPort(new DefaultPortModel(true, "in-1", "In"));
node4.setPosition(95, 185);
const node5 = new DefaultNodeModel("Node E", "rgb(192,255,0)");
node5.setPosition(250, 180);
// linking things together
const link1 = port1.link(port4);
const link2 = port2.link(port3);
// add all to the main model
model.addAll(node1, node2, node3, node4, node5, link1, link2);
// load model into engine and render
engine.setModel(model);
return (
<DemoWorkspaceWidget
buttons={
<button
onClick={() => {
action("Serialized Graph")(JSON.stringify(model.serializeDiagram(), null, 2));
}}
>
Serialize Graph
</button>
}
>
<DiagramWidget
className="srd-demo-canvas"
diagramEngine={engine}
smartRouting={true}
maxNumberPointsPerLink={0}
/>
</DemoWorkspaceWidget>
);
};

View File

@@ -1,13 +1,11 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "../../src/main";
} from "storm-react-diagrams";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
@@ -20,31 +18,7 @@ import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addPort(new DefaultPortModel(false, "out-1", "Out"));
node1.x = 100 + offsetX;
node1.y = 100 + offsetY;
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addPort(new DefaultPortModel(true, "in-1", "IN"));
node2.x = 200 + offsetX;
node2.y = 100 + offsetY;
//3-C) link the 2 nodes together
var link1 = new LinkModel();
link1.setSourcePort(port1);
link1.setTargetPort(port2);
//4) add the models to the root graph
model.addNode(node1);
model.addNode(node2);
model.addLink(link1);
}
engine.installDefaults();
//2) setup the diagram model
var model = new DiagramModel();
@@ -56,12 +30,32 @@ export default () => {
}
//5) load model into engine
engine.setDiagramModel(model);
engine.setModel(model);
//6) render the diagram!
return (
<DemoWorkspaceWidget buttons={<button onClick={() => engine.zoomToFit()}>Zoom to fit</button>}>
<DiagramWidget diagramEngine={engine} />
<DemoWorkspaceWidget
buttons={<button onClick={() => engine.getCanvasWidget().zoomToFit()}>Zoom to fit</button>}
>
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
};
function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) {
//3-A) create a default node
var node1 = new DefaultNodeModel("Node 1", "rgb(0,192,255)");
var port1 = node1.addOutPort("Out");
node1.setPosition(100 + offsetX, 100 + offsetY);
//3-B) create another default node
var node2 = new DefaultNodeModel("Node 2", "rgb(192,255,0)");
var port2 = node2.addInPort("In");
node2.setPosition(200 + offsetX, 100 + offsetY);
//3-C) link the 2 nodes together
var link1 = port1.link(port2);
//4) add the models to the root graph
model.addAll(node1, node2, link1);
}

View File

@@ -1,15 +1,14 @@
import * as React from "react";
import { storiesOf, addDecorator } from "@storybook/react";
import { setOptions } from "@storybook/addon-options";
import { Toolkit } from "../src/Toolkit";
import { host } from "storybook-host";
import { Helper } from "./.helpers/Helper";
import { Toolkit } from "../src/Toolkit";
//include the SCSS for the demo
require("./.helpers/demo.scss");
import "./.helpers/demo.scss";
// make tests deterministic
Toolkit.TESTING_MODE = true;
Toolkit.TESTING = true;
addDecorator(
host({
@@ -35,78 +34,96 @@ storiesOf("Simple Usage", module)
require("./demo-simple/docs.md")
)
)
.add(
"Performance demo",
Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))
)
.add(
"Locked widget",
Helper.makeDemo(require("./demo-locks/index").default(), require("!!raw-loader!./demo-locks/index"))
)
.add(
"Canvas grid size",
Helper.makeDemo(require("./demo-grid/index").default(), require("!!raw-loader!./demo-grid/index"))
)
.add(
"Limiting link points",
Helper.makeDemo(
require("./demo-limit-points/index").default(),
require("!!raw-loader!./demo-limit-points/index")
)
)
.add(
"Events and listeners",
Helper.makeDemo(require("./demo-listeners/index").default(), require("!!raw-loader!./demo-listeners/index"))
)
.add(
"Zoom to fit",
Helper.makeDemo(require("./demo-zoom-to-fit/index").default(), require("!!raw-loader!./demo-zoom-to-fit/index"))
);
storiesOf("Advanced Techniques", module)
.add(
"Clone Selected",
Helper.makeDemo(require("./demo-cloning/index").default(), require("!!raw-loader!./demo-cloning/index"))
)
.add(
"Serializing and de-serializing",
Helper.makeDemo(require("./demo-serializing/index").default(), require("!!raw-loader!./demo-serializing/index"))
)
.add(
"Programatically modifying graph",
Helper.makeDemo(
require("./demo-mutate-graph/index").default(),
require("!!raw-loader!./demo-mutate-graph/index")
)
)
.add(
"Large application",
Helper.makeDemo(
require("./demo-drag-and-drop/index").default(),
require("!!raw-loader!./demo-drag-and-drop/components/BodyWidget")
)
);
storiesOf("Custom Models", module)
.add(
"Custom diamond node",
Helper.makeDemo(
require("./demo-custom-node1/index").default(),
require("!!raw-loader!./demo-custom-node1/index")
)
)
.add(
"Custom link sizes",
Helper.makeDemo(
require("./demo-custom-link1/index").default(),
require("!!raw-loader!./demo-custom-link1/index")
)
);
storiesOf("3rd party libraries", module).add(
"Auto Distribute (Dagre)",
Helper.makeDemo(require("./demo-dagre/index").default(), require("!!raw-loader!./demo-dagre/index"))
);
// .add(
// "Simple flow example",
// Helper.makeDemo(require("./demo-simple-flow/index").default(), require("!!raw-loader!./demo-simple-flow/index"))
// )
// .add(
// "Performance demo",
// Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))
// )
// .add(
// "Locked widget",
// Helper.makeDemo(require("./demo-locks/index").default(), require("!!raw-loader!./demo-locks/index"))
// )
// .add(
// "Canvas grid size",
// Helper.makeDemo(require("./demo-grid/index").default(), require("!!raw-loader!./demo-grid/index"))
// )
// .add(
// "Limiting link points",
// Helper.makeDemo(
// require("./demo-limit-points/index").default(),
// require("!!raw-loader!./demo-limit-points/index")
// )
// )
// .add(
// "Events and listeners",
// Helper.makeDemo(require("./demo-listeners/index").default(), require("!!raw-loader!./demo-listeners/index"))
// )
// .add(
// "Zoom to fit",
// Helper.makeDemo(require("./demo-zoom-to-fit/index").default(), require("!!raw-loader!./demo-zoom-to-fit/index"))
// )
// .add(
// "Links with labels",
// Helper.makeDemo(
// require("./demo-labelled-links/index").default(),
// require("!!raw-loader!./demo-labelled-links/index")
// )
// );
//
// storiesOf("Advanced Techniques", module)
// .add(
// "Clone Selected",
// Helper.makeDemo(require("./demo-cloning/index").default(), require("!!raw-loader!./demo-cloning/index"))
// )
// .add(
// "Serializing and de-serializing",
// Helper.makeDemo(require("./demo-serializing/index").default(), require("!!raw-loader!./demo-serializing/index"))
// )
// .add(
// "Programatically modifying graph",
// Helper.makeDemo(
// require("./demo-mutate-graph/index").default(),
// require("!!raw-loader!./demo-mutate-graph/index")
// )
// )
// .add(
// "Large application",
// Helper.makeDemo(
// require("./demo-drag-and-drop/index").default(),
// require("!!raw-loader!./demo-drag-and-drop/components/BodyWidget")
// )
// )
// .add(
// "Smart routing",
// Helper.makeDemo(
// require("./demo-smart-routing/index").default(),
// require("!!raw-loader!./demo-smart-routing/index")
// )
// );
//
// storiesOf("Custom Models", module)
// .add(
// "Custom diamond node",
// Helper.makeDemo(
// require("./demo-custom-node1/index").default(),
// require("!!raw-loader!./demo-custom-node1/index")
// )
// )
// .add(
// "Custom animated links",
// Helper.makeDemo(
// require("./demo-custom-link1/index").default(),
// require("!!raw-loader!./demo-custom-link1/index")
// )
// );
//
// storiesOf("3rd party libraries", module).add(
// "Auto Distribute (Dagre)",
// Helper.makeDemo(require("./demo-dagre/index").default(), require("!!raw-loader!./demo-dagre/index"))
// );
// enable this to log mouse location when writing new puppeteer tests
//Helper.logMousePosition()

10
demos/tslint.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": [
"../tslint.json"
],
"rules": {
"no-console": false,
"max-classes-per-file": false,
"no-var-requires": false
}
}

View File

@@ -0,0 +1,76 @@
# Architecture Questions
Here I will try to answer any questions relating to the design of the system
### What was the inspiration for this library?
Joint JS (a fantastic library) + my need for rich HTML nodes + LabView + Blender Composite sub system
### Why render the nodes as HTML Elements and not SVG's?
My original requirement for this library stemmed from the requirement of wanting HTML nodes that would allow me to
embed rich controls such as input fields, drop downs and have the system treat such nodes as first class citizens.
I originally tried to make this work in JointJS, but ran into a number of problems of which this was a relatively big one.
JointJS does allow you to do this, but at the time of writing this library originally, I was having a lot of trouble
to make it work exactly like I needed it, and therefore decided from the very beginning that I would attempt this
with an HTML first mindset.
### Why Typescript?
Firstly, because it can transpile into any level of ECMAScript. This means that I don't need to break our the refactor tractor
every time ECMAScript decides it wants to add features which it should have done years ago.
I also ported it to Typescript to accommodate the heavy architectural changes I was starting to make. Since porting
the library to typescript, and seeing the project explode in size and complexity, I consider this the best decision
made with regards to this library so far.
Porting to typescript also afforded us a set of powerful features such as generics and static analysis that
all the project contributors have made exclusive use of.
Typescript is <3 typescript is life.
### Why not Flow instead of Typescript?
At the time when I first started evaluating languages that could transpile to ECMAScript, I was not so sold
on the supporting environment surrounding flow, and and found that there was better tooling to support
typescript, they are ultimately trying to do the same thing though, and I guess in the end, typescript just made
more sense.
### Why React ?
React is really efficient at rendering and managing HTML in a declarative manner. React has also become
one of the bigger industry standards and has a rich eco system that plays really well with typescript.
Apart from these notable points, I am really fond of React and wanted a diagramming library that takes full advantage of it,
and makes it easy for engineers to use its power as well, when extending this library.
### Why cant the Default models and widgets do this or that ?
They are intended to illustrate __how__ to use this library and act as a good starting point
to extend and show the capability. Ultimately I designed this library to be completely
pluggable in a way that you can use it as a library and not a framework. If the default widgets
are not good enough, then a good place to start is with creating your own models/factories/widgets.
### Model vs Widget
For those that are new to [Scene Graphs](https://en.wikipedia.org/wiki/Scene_graph) or are not familiar with concepts
such as [MVC](https://en.wikipedia.org/wiki/Modelviewcontroller), this library represents your entire graph as a model.
The model is a traversable graph that represents the nodes and links between them in a virtual manner. Your program (aka the business logic/layer)
can mutate this model imperatively or store snapshots decoratively of the complete model (via serialization) and then the engine and react
widgets will take care of the rendering. For this reason every model in the library is represented by a widget, and the factories glue it all together.
### How do I make my own elements?
Take a look at the __demos__ directory, with specific attention to the __DefaultNodeWidget__
That being said, the demos 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 models/factories/widgets.
### 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

142
docs/Getting Started.md Normal file
View File

@@ -0,0 +1,142 @@
# Getting started
## Installation via NPM
The first thing you need to do, is grab the distribution files on NPM. You can do this either using yarn or npm
**Via yarn:**
```
yarn install storm-react-diagrams
```
**Via npm:**
```
npm install storm-react-diagrams
```
When you run this in your project directory, this will install the library into node\_modules/storm-react-diagrams. You will then find a dist folder that contains all the minified and production ready code.
This will also install React and a few other dependencies that you need in order to use this library.
## Including the library
When including the library you will need both the javascript files as well as the raw BEM styles. Both are included in the dist folder and there are numerous ways to integrate them into your project:
#### Getting the javascript files
**Using Typescript / ES6: \(recommended\)**
```js
import * as SRD from "storm-react-diagrams"
```
**Using RequireJS:**
```js
var SRD = require("storm-react-diagrams)
```
**As a script tag \(not recommended\)**
```html
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="node_modules/storm-react-diagrams/dist/main.js">
```
#### Getting the CSS
**Using RequireJS / Typescript/ ES6 \(recommended\)**
Make sure you have the style-loader enabled and then:
```js
require("storm-react-diagrams/dist/style.min.css");
```
or make sure you have the sass-loader enabled and then:
```js
require("storm-react-diagrams/src/sass/main.scss");
```
If you are using typescript and get a "require function not found" then make sure to
```
yarn add @types/node
```
which will give you the typescript definition files for requireJS
**Using SASS:**
setup your include paths on webpack or lib sass using the following option
```
includePaths: ["node_modules"]
```
and then if you want the raw sass source code:
```sass
@import "~storm-react-diagrams/src/sass/main";
```
or if you want the minified css
```sass
@import "~storm-react-diagrams/dist/style.min";
```
**Using a style tag**
or if you want the minified css
```html
<link rel="stylesheet" href="node_modules/dist/style.min.css">
```
## Render your first diagram
In your library code
```js
// 1) setup the diagram engine
var engine = new SRD.DiagramEngine();
engine.installDefaultFactories();
// 2) setup the diagram model
var model = new SRD.DiagramModel();
// 3) create a default node
var node1 = new SRD.DefaultNodeModel("Node 1", "rgb(0,192,255)");
let port1 = node1.addOutPort("Out");
node1.setPosition(100, 100);
// 4) create another default node
var node2 = new SRD.DefaultNodeModel("Node 2", "rgb(192,255,0)");
let port2 = node2.addInPort("In");
node2.setPosition(400, 100);
// 5) link the ports
let link1 = port1.link(port2);
// 6) add the models to the root graph
model.addAll(node1, node2, link1);
// 7) load model into engine
engine.setDiagramModel(model);
```
And then create an instance of the diagram widget. An example of the simplest possible react widget to do this would be:
```jsx
function SimpleDiagramWidget(props) {
return <SRD.DiagramWidget diagramEngine={props.engine} />;
}
```

22
docs/Interactive Usage.md Normal file
View File

@@ -0,0 +1,22 @@
# End user usage
__Delete__ removes any selected items
![__Delete__](../images/rjdDelete.gif)
__Shift + Mouse Drag__ triggers a multi-selection box
![Shift + Mouse Drag](../images/mouseDrag.gif)
__Shift + Mouse Click__ selects the item (items can be multi-selected)
![Shift + Mouse Click](../images/shiftClick.gif)
__Mouse Drag__ drags the entire diagram
![Mouse Drag](../images/canvasDrag.gif)
__Mouse Wheel__ zooms the diagram in / out
![Mouse Wheel](../images/mouseWheel.gif)
__Click Link + Drag__ creates a new link point
![Click Link + Drag](../images/createPoint.gif)
__Click Node Port + Drag__ creates a new link
![Click Node Port + Drag](../images/createLink.gif)

View File

@@ -1,24 +0,0 @@
## Questions
#### Why didnt 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
#### 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

33
docs/Testing.md Normal file
View File

@@ -0,0 +1,33 @@
# Testing
STORM React diagrams is tested two main ways.
## JEST Snapshot testing
With Jest snapshots, we render all the demos in the demo folder by automatically
looking into each `demo-*` folder and searching for an __index.tsx__ file.
For each file we find, we dynamically include it as a storybook story and assemble one big test.
This test then renders each demo in a deterministic way and compares it to the snapshot file
situated in __snapshots__. If the snapshots don't match, then something has changed and either the snapshot
needs to be updated, or the test is failing in which case we need to fix the code.
Snapshot testing does not test the functionality of the program but it is a first important step
to make sure that changes aren't having a drastic effect on the overall system.
In the event that the snapshot needs to be updated, please run `yarn run test -u` to overwrite
the snapshot file, and then make sure to your branch.
## End to end testing
To test the functionality of the library, we make use of e2e tests (end to end tests).
In this library, we spin up a headless chrome using pupeteer and interactively and programmatically
tell the mouse pointer to click and drag on various elements while making assertions along the way.
We use Jest for the assertions and the interactivity is handled by puppeteer. Due to the laborious nature
of writing e2e tests, there is a helper method that is provided in each test that makes interacting
with the diagrams a lot easier. Using this helper, you cna easily tell the mouse to drag links between nodes,
select them and also easily assert information about them. The important thing here, is that this helper
does not touch the model in any way, but is purely a helper for writing the tests themselves. Please
make use of this helper when writing tests, as it ensure that the tests are defensive in nature, and also
reduces the overhead of physically writing them.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

BIN
images/example1.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

BIN
images/example2.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

BIN
images/example3.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,12 +1,27 @@
const path = require("path");
// jest.config.js
module.exports = {
verbose: true,
moduleFileExtensions: ["ts", "tsx", "js", "json"],
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
transform: {
"^.+\\.(ts|tsx)$": "./tests/helpers/tsx-preprocessor.js",
"^.+\\.(scss)$": "./tests/helpers/scss-preprocessor.js"
".*test_loader.*": path.join(__dirname, "tests", "helpers", "storybook-loader.js" ),
"^.+\\.tsx?$": "ts-jest",
},
"testMatch": [
"**/tests/*\.test\.*"
moduleNameMapper:{
"\\.(scss|css|png)$": path.join(__dirname,"tests","helpers","css-mock.js"),
"storm-react-diagrams": path.join(__dirname, "src", "main")
},
roots:[
__dirname+'/tests'
],
testMatch: [
"**/*\.test\.tsx"
]
};

View File

@@ -1,6 +1,6 @@
{
"name": "storm-react-diagrams",
"version": "4.0.0",
"version": "5.1.1",
"repository": {
"type": "git",
"url": "https://github.com/projectstorm/react-diagrams.git"
@@ -17,55 +17,72 @@
"nodes"
],
"main": "./dist/main.js",
"typings": "./dist/src/main",
"typings": "./dist/@types/src/main",
"author": "dylanvorster",
"scripts": {
"test": "jest --no-cache",
"build:ts": "webpack",
"build:ts:prod": "cross-env NODE_ENV=production webpack",
"build:sass:prod": "node-sass --output-style compressed ./src/sass/main.scss > ./dist/style.min.css",
"storybook": "start-storybook -p 9001 -c .storybook",
"storybook:build": "build-storybook -c .storybook -o .out",
"storybook:github": "storybook-to-ghpages",
"prepublishOnly": "export NODE_ENV=production && webpack && ./node_modules/node-sass/bin/node-sass --output-style compressed ./src/sass.scss > ./dist/style.min.css",
"pretty": "prettier --use-tabs --write \"{src,demos}/**/*.{ts,tsx}\" --print-width 120"
"pretty": "prettier --use-tabs --write \"{src,demos,tests}/**/*.{ts,tsx}\" --print-width 120",
"lint": "tslint -p .",
"test:ci": "rm -rf ./dist && node ./tests/e2e/generate-e2e.js && jest --no-cache",
"test": "jest --no-cache",
"prepublishOnly": "yarn run build:ts:prod && yarn run build:sass:prod"
},
"dependencies": {
"closest": "^0.0.1",
"lodash": "^4.17.4",
"lodash": "^4.17.5",
"pathfinding": "^0.4.18",
"paths-js": "^0.4.7",
"react": "^16.2.0"
},
"devDependencies": {
"@storybook/addon-actions": "^3.3.9",
"@storybook/addon-options": "^3.3.9",
"@storybook/addon-storyshots": "^3.3.9",
"@storybook/addons": "^3.3.9",
"@storybook/react": "^3.3.9",
"@storybook/storybook-deployer": "^2.2.0",
"@types/jest": "^22.0.1",
"@types/lodash": "^4.14.92",
"@types/node": "^9.3.0",
"@types/puppeteer": "^0.13.9",
"@types/react": "^16.0.34",
"awesome-typescript-loader": "^3.4.1",
"css-loader": "^0.28.9",
"dagre": "^0.8.1",
"file-loader": "^1.1.6",
"jest": "^22.1.3",
"jest-cli": "^22.1.3",
"@storybook/addon-actions": "^3.3.15",
"@storybook/addon-options": "^3.3.15",
"@storybook/addon-storyshots": "^3.3.15",
"@storybook/addons": "^3.3.15",
"@storybook/react": "^3.3.15",
"@storybook/storybook-deployer": "^2.3.0",
"@types/jest": "^22.2.0",
"@types/lodash": "^4.14.104",
"@types/node": "^9.4.7",
"@types/promise": "^7.1.30",
"@types/puppeteer": "^1.1.0",
"@types/react": "^16.0.40",
"awesome-typescript-loader": "^4.0.1",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.4",
"css-loader": "^0.28.10",
"dagre": "^0.8.2",
"enzyme": "^3.3.0",
"file-loader": "^1.1.11",
"glob": "^7.1.2",
"jest": "^22.4.2",
"jest-cli": "^22.4.2",
"json-beautify": "^1.0.1",
"node-sass": "^4.7.2",
"prettier": "^1.10.2",
"puppeteer": "^1.0.0",
"prettier": "^1.11.1",
"puppeteer": "^1.1.1",
"raf": "^3.4.0",
"raw-loader": "^0.5.1",
"react-dom": "^16.2.0",
"react-syntax-highlighter": "^6.1.2",
"react-syntax-highlighter": "^7.0.2",
"react-test-renderer": "^16.2.0",
"sass-loader": "^6.0.6",
"sass-loader": "^6.0.7",
"source-map-loader": "^0.2.3",
"storybook-host": "^4.1.5",
"storybook-readme": "^3.2.0",
"style-loader": "^0.19.1",
"storybook-readme": "^3.2.1",
"style-loader": "^0.20.3",
"ts-jest": "^22.4.1",
"tslint": "^5.9.1",
"typescript": "^2.6.2",
"webpack": "^3.10.0"
"ts-loader": "^4.1.0",
"typescript": "^2.7.2",
"uglifyjs-webpack-plugin": "^1.2.3",
"val-loader": "^1.1.0",
"webpack": "^4.1.1",
"webpack-cli": "^2.0.11"
}
}

View File

@@ -1,42 +0,0 @@
import { BaseModel } from "./models/BaseModel";
import { NodeModel } from "./models/NodeModel";
import { LinkModel } from "./models/LinkModel";
import { DiagramEngine } from "./DiagramEngine";
import { PortModel } from "./models/PortModel";
export abstract class AbstractFactory<T extends BaseModel> {
type: string;
constructor(name: string) {
this.type = name;
}
getType(): string {
return this.type;
}
abstract getNewInstance(initialConfig?: any): T;
}
export abstract class NodeFactory<T extends NodeModel = NodeModel> extends AbstractFactory<T> {
abstract generateReactWidget(diagramEngine: DiagramEngine, node: T): JSX.Element;
}
export abstract class LinkFactory<T extends LinkModel = LinkModel> extends AbstractFactory<T> {
abstract generateReactWidget(diagramEngine: DiagramEngine, link: T): JSX.Element;
}
export abstract class PortFactory<T extends PortModel = PortModel> extends AbstractFactory<T> {}
export class SimplePortFactory extends PortFactory {
cb: (initialConfig?: any) => PortModel;
constructor(type: string, cb: (initialConfig?: any) => PortModel) {
super(type);
this.cb = cb;
}
getNewInstance(initialConfig?: any): PortModel {
return this.cb(initialConfig);
}
}

View File

@@ -1,108 +0,0 @@
import { Toolkit } from "./Toolkit";
import * as _ from "lodash";
/**
* @author Dylan Vorster
*/
export interface BaseEvent<T extends BaseEntity = any> {
entity: BaseEntity<BaseListener>;
stopPropagation: () => any;
firing: boolean;
id: string;
}
export interface BaseListener<T extends BaseEntity = any> {
lockChanged?(event: BaseEvent<T> & { locked: boolean }): void;
}
export type BaseEntityType = "node" | "link" | "port" | "point";
export class BaseEntity<T extends BaseListener = {}> {
public listeners: { [s: string]: T };
public id: string;
public locked: boolean;
constructor(id?: string) {
this.listeners = {};
this.id = id || Toolkit.UID();
this.locked = false;
}
getID() {
return this.id;
}
doClone(lookupTable = {}, clone) {}
clone(lookupTable = {}) {
// try and use an existing clone first
if (lookupTable[this.id]) {
return lookupTable[this.id];
}
let clone = _.clone(this);
clone.id = Toolkit.UID();
clone.clearListeners();
lookupTable[this.id] = clone;
this.doClone(lookupTable, clone);
return clone;
}
clearListeners() {
this.listeners = {};
}
public deSerialize(data) {
this.id = data.id;
}
public serialize() {
return {
id: this.id
};
}
public iterateListeners(cb: (t: T, event: BaseEvent) => any) {
let event: BaseEvent = {
id: Toolkit.UID(),
firing: true,
entity: this,
stopPropagation: () => {
event.firing = false;
}
};
for (var i in this.listeners) {
// propagation stopped
if (!event.firing) {
return;
}
cb(this.listeners[i], event);
}
}
public removeListener(listener: string) {
if (this.listeners[listener]) {
delete this.listeners[listener];
return true;
}
return false;
}
public addListener(listener: T): string {
var uid = Toolkit.UID();
this.listeners[uid] = listener;
return uid;
}
public isLocked(): boolean {
return this.locked;
}
public setLocked(locked: boolean = true) {
this.locked = locked;
this.iterateListeners((listener, event) => {
if (listener.lockChanged) {
listener.lockChanged({ ...event, locked: locked });
}
});
}
}

View File

@@ -1,87 +0,0 @@
import { DiagramModel } from "./models/DiagramModel";
import { DiagramEngine } from "./DiagramEngine";
import { SelectionModel } from "./widgets/DiagramWidget";
import { PointModel } from "./models/PointModel";
import { NodeModel } from "./models/NodeModel";
export class BaseAction {
mouseX: number;
mouseY: number;
ms: number;
constructor(mouseX: number, mouseY: number) {
this.mouseX = mouseX;
this.mouseY = mouseY;
this.ms = new Date().getTime();
}
}
export class SelectingAction extends BaseAction {
mouseX2: number;
mouseY2: number;
constructor(mouseX: number, mouseY: number) {
super(mouseX, mouseY);
this.mouseX2 = mouseX;
this.mouseY2 = mouseY;
}
getBoxDimensions() {
return {
left: this.mouseX2 > this.mouseX ? this.mouseX : this.mouseX2,
top: this.mouseY2 > this.mouseY ? this.mouseY : this.mouseY2,
width: Math.abs(this.mouseX2 - this.mouseX),
height: Math.abs(this.mouseY2 - this.mouseY),
right: this.mouseX2 < this.mouseX ? this.mouseX : this.mouseX2,
bottom: this.mouseY2 < this.mouseY ? this.mouseY : this.mouseY2
};
}
containsElement(x: number, y: number, diagramModel: DiagramModel): boolean {
var z = diagramModel.getZoomLevel() / 100.0;
let dimensions = this.getBoxDimensions();
return (
x * z + diagramModel.getOffsetX() > dimensions.left &&
x * z + diagramModel.getOffsetX() < dimensions.right &&
y * z + diagramModel.getOffsetY() > dimensions.top &&
y * z + diagramModel.getOffsetY() < dimensions.bottom
);
}
}
export class MoveCanvasAction extends BaseAction {
initialOffsetX: number;
initialOffsetY: number;
constructor(mouseX: number, mouseY: number, diagramModel: DiagramModel) {
super(mouseX, mouseY);
this.initialOffsetX = diagramModel.getOffsetX();
this.initialOffsetY = diagramModel.getOffsetY();
}
}
export class MoveItemsAction extends BaseAction {
selectionModels: SelectionModel[];
moved: boolean;
constructor(mouseX: number, mouseY: number, diagramEngine: DiagramEngine) {
super(mouseX, mouseY);
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,
initialX: item.x,
initialY: item.y
};
});
}
}

View File

@@ -1,271 +1,41 @@
import { BaseEntity, BaseListener } from "./BaseEntity";
import { DiagramModel } from "./models/DiagramModel";
import * as _ from "lodash";
import { BaseModel, BaseModelListener } from "./models/BaseModel";
import { NodeModel } from "./models/NodeModel";
import { PointModel } from "./models/PointModel";
import { PortModel } from "./models/PortModel";
import { LinkModel } from "./models/LinkModel";
import { LinkFactory, NodeFactory, PortFactory } from "./AbstractFactory";
import { DefaultLinkFactory, DefaultNodeFactory } from "./main";
import { DefaultPortFactory } from "./defaults/DefaultPortFactory";
/**
* @author Dylan Vorster
*/
export interface DiagramEngineListener extends BaseListener {
portFactoriesUpdated?(): void;
import { CanvasEngine } from "@projectstorm/react-canvas";
import { DefaultLabelFactory, DefaultLinkFactory, DefaultNodeFactory, DefaultPortFactory } from "storm-react-diagrams";
nodeFactoriesUpdated?(): void;
linkFactoriesUpdated?(): void;
repaintCanvas?(): void;
}
/**
* Passed as a parameter to the DiagramWidget
*/
export class DiagramEngine extends BaseEntity<DiagramEngineListener> {
nodeFactories: { [s: string]: NodeFactory };
linkFactories: { [s: string]: LinkFactory };
portFactories: { [s: string]: PortFactory };
diagramModel: DiagramModel;
canvas: Element;
export class DiagramEngine extends CanvasEngine<DiagramModel> {
paintableWidgets: {};
linksThatHaveInitiallyRendered: {};
nodesRendered: boolean;
maxNumberPointsPerLink: number;
smartRouting: boolean;
constructor() {
super();
this.diagramModel = new DiagramModel();
this.nodeFactories = {};
this.linkFactories = {};
this.portFactories = {};
this.canvas = null;
this.paintableWidgets = null;
this.linksThatHaveInitiallyRendered = {};
this.smartRouting = false;
}
installDefaultFactories() {
this.registerNodeFactory(new DefaultNodeFactory());
this.registerLinkFactory(new DefaultLinkFactory());
this.registerPortFactory(new DefaultPortFactory());
}
repaintCanvas() {
this.iterateListeners(listener => {
listener.repaintCanvas && listener.repaintCanvas();
});
}
clearRepaintEntities() {
this.paintableWidgets = null;
}
enableRepaintEntities(entities: BaseModel<BaseModelListener>[]) {
this.paintableWidgets = {};
entities.forEach(entity => {
//if a node is requested to repaint, add all of its links
if (entity instanceof NodeModel) {
_.forEach(entity.getPorts(), port => {
_.forEach(port.getLinks(), link => {
this.paintableWidgets[link.getID()] = true;
});
});
}
if (entity instanceof PointModel) {
this.paintableWidgets[entity.getLink().getID()] = true;
}
this.paintableWidgets[entity.getID()] = true;
});
}
/**
* Checks to see if a model is locked by running through
* its parents to see if they are locked first
*/
isModelLocked(model: BaseEntity<BaseListener>) {
//always check the diagram model
if (this.diagramModel.isLocked()) {
return true;
}
return model.isLocked();
}
recalculatePortsVisually() {
this.nodesRendered = false;
this.linksThatHaveInitiallyRendered = {};
}
canEntityRepaint(baseModel: BaseModel<BaseModelListener>) {
//no rules applied, allow repaint
if (this.paintableWidgets === null) {
return true;
}
return this.paintableWidgets[baseModel.getID()] !== undefined;
}
setCanvas(canvas: Element | null) {
this.canvas = canvas;
}
setDiagramModel(model: DiagramModel) {
this.diagramModel = model;
this.recalculatePortsVisually();
}
getDiagramModel(): DiagramModel {
return this.diagramModel;
}
getNodeFactories(): { [s: string]: NodeFactory } {
return this.nodeFactories;
}
getLinkFactories(): { [s: string]: LinkFactory } {
return this.linkFactories;
}
registerPortFactory(factory: PortFactory) {
this.portFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.portFactoriesUpdated) {
listener.portFactoriesUpdated();
}
});
}
registerNodeFactory(factory: NodeFactory) {
this.nodeFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.nodeFactoriesUpdated) {
listener.nodeFactoriesUpdated();
}
});
}
registerLinkFactory(factory: LinkFactory) {
this.linkFactories[factory.getType()] = factory;
this.iterateListeners(listener => {
if (listener.linkFactoriesUpdated) {
listener.linkFactoriesUpdated();
}
});
}
getPortFactory(type: string): PortFactory {
if (this.portFactories[type]) {
return this.portFactories[type];
}
console.log("cannot find factory for port of type: [" + type + "]");
return null;
}
getNodeFactory(type: string): NodeFactory {
if (this.nodeFactories[type]) {
return this.nodeFactories[type];
}
console.log("cannot find factory for node of type: [" + type + "]");
return null;
}
getLinkFactory(type: string): LinkFactory {
if (this.linkFactories[type]) {
return this.linkFactories[type];
}
console.log("cannot find factory for link of type: [" + type + "]");
return null;
}
getFactoryForNode(node: NodeModel): NodeFactory | null {
return this.getNodeFactory(node.getType());
}
getFactoryForLink(link: LinkModel): LinkFactory | null {
return this.getLinkFactory(link.getType());
}
generateWidgetForLink(link: LinkModel): JSX.Element | null {
var linkFactory = this.getFactoryForLink(link);
if (!linkFactory) {
throw new Error("Cannot find link factory for link: " + link.getType());
}
return linkFactory.generateReactWidget(this, link);
}
generateWidgetForNode(node: NodeModel): JSX.Element | null {
var nodeFactory = this.getFactoryForNode(node);
if (!nodeFactory) {
throw new Error("Cannot find widget factory for node: " + node.getType());
}
return nodeFactory.generateReactWidget(this, node);
}
getRelativeMousePoint(event): { x: number; y: number } {
var point = this.getRelativePoint(event.clientX, event.clientY);
return {
x: (point.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
y: (point.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
};
}
getRelativePoint(x, y) {
var canvasRect = this.canvas.getBoundingClientRect();
return { x: x - canvasRect.left, y: y - canvasRect.top };
}
getNodePortElement(port: PortModel): any {
var selector = this.canvas.querySelector(
'.port[data-name="' + port.getName() + '"][data-nodeid="' + port.getParent().getID() + '"]'
);
if (selector === null) {
throw new Error(
"Cannot find Node Port element with nodeID: [" +
port.getParent().getID() +
"] and name: [" +
port.getName() +
"]"
);
}
return selector;
}
getPortCenter(port: PortModel) {
var sourceElement = this.getNodePortElement(port);
var sourceRect = sourceElement.getBoundingClientRect();
var rel = this.getRelativePoint(sourceRect.left, sourceRect.top);
return {
x:
sourceElement.offsetWidth / 2 +
(rel.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
y:
sourceElement.offsetHeight / 2 +
(rel.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
};
installDefaults() {
super.installDefaults();
this.registerElementFactory(new DefaultLabelFactory());
this.registerElementFactory(new DefaultLinkFactory());
this.registerElementFactory(new DefaultNodeFactory());
this.registerElementFactory(new DefaultPortFactory());
}
getMaxNumberPointsPerLink(): number {
return this.maxNumberPointsPerLink;
}
setMaxNumberPointsPerLink(max: number) {
this.maxNumberPointsPerLink = max;
}
zoomToFit() {
const xFactor = this.canvas.clientWidth / this.canvas.scrollWidth;
const yFactor = this.canvas.clientHeight / this.canvas.scrollHeight;
const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
isSmartRoutingEnabled() {
return this.smartRouting;
}
this.diagramModel.setZoomLevel(this.diagramModel.getZoomLevel() * zoomFactor);
this.diagramModel.setOffset(0, 0);
this.repaintCanvas();
setSmartRoutingStatus(status: boolean) {
this.smartRouting = status;
}
}

View File

@@ -1,27 +1,13 @@
// tslint:disable no-bitwise
import closest = require("closest");
import { PointModel } from "./models/PointModel";
import { ROUTING_SCALING_FACTOR } from "./routing/PathFinding";
import * as Path from "paths-js/path";
import { Toolkit as TK } from "@projectstorm/react-canvas";
/**
* @author Dylan Vorster
*/
export class Toolkit {
static TESTING_MODE = false;
static TESTING_MODE_ID = 1;
/**
* Generats a unique ID (thanks Stack overflow :3)
* @returns {String}
*/
public static UID(): string {
if (Toolkit.TESTING_MODE) {
Toolkit.TESTING_MODE_ID++;
return "test-uid-" + Toolkit.TESTING_MODE_ID;
}
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);
});
}
export class Toolkit extends TK {
/**
* Finds the closest element as a polyfill
*
@@ -34,4 +20,26 @@ export class Toolkit {
}
return closest(element, selector);
}
public static generateLinePath(firstPoint: PointModel, lastPoint: PointModel): string {
return `M${firstPoint.x},${firstPoint.y} L ${lastPoint.x},${lastPoint.y}`;
}
public static generateCurvePath(firstPoint: PointModel, lastPoint: PointModel, curvy: number = 0): string {
var isHorizontal = Math.abs(firstPoint.x - lastPoint.x) > Math.abs(firstPoint.y - lastPoint.y);
var curvyX = isHorizontal ? curvy : 0;
var curvyY = isHorizontal ? 0 : curvy;
return `M${firstPoint.x},${firstPoint.y} C ${firstPoint.x + curvyX},${firstPoint.y + curvyY}
${lastPoint.x - curvyX},${lastPoint.y - curvyY} ${lastPoint.x},${lastPoint.y}`;
}
public static generateDynamicPath(pathCoords: number[][]) {
let path = Path();
path = path.moveto(pathCoords[0][0] * ROUTING_SCALING_FACTOR, pathCoords[0][1] * ROUTING_SCALING_FACTOR);
pathCoords.slice(1).forEach(coords => {
path = path.lineto(coords[0] * ROUTING_SCALING_FACTOR, coords[1] * ROUTING_SCALING_FACTOR);
});
return path.print();
}
}

View File

@@ -1,24 +0,0 @@
import * as React from "react";
import { DefaultLinkWidget } from "./DefaultLinkWidget";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "../models/LinkModel";
import { LinkFactory } from "../AbstractFactory";
/**
* @author Dylan Vorster
*/
export class DefaultLinkFactory extends LinkFactory {
constructor() {
super("default");
}
generateReactWidget(diagramEngine: DiagramEngine, link: LinkModel): JSX.Element {
return React.createElement(DefaultLinkWidget, {
link: link,
diagramEngine: diagramEngine
});
}
getNewInstance(initialConfig?: any): LinkModel {
return new LinkModel();
}
}

View File

@@ -1,221 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "../models/LinkModel";
import { PointModel } from "../models/PointModel";
export interface DefaultLinkProps {
color?: string;
width?: number;
smooth?: boolean;
link: LinkModel;
diagramEngine: DiagramEngine;
pointAdded?: (point: PointModel, event) => any;
}
export interface DefaultLinkState {
selected: boolean;
}
/**
* @author Dylan Vorster
*/
export class DefaultLinkWidget extends React.Component<DefaultLinkProps, DefaultLinkState> {
public static defaultProps: DefaultLinkProps = {
color: "black",
width: 3,
link: null,
engine: null,
smooth: false,
diagramEngine: null
};
constructor(props: DefaultLinkProps) {
super(props);
this.state = {
selected: false
};
}
addPointToLink = (event, index: number): void => {
if (
!event.shiftKey &&
!this.props.diagramEngine.isModelLocked(this.props.link) &&
this.props.link.points.length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink()
) {
const point = new PointModel(this.props.link, this.props.diagramEngine.getRelativeMousePoint(event));
point.setSelected(true);
this.forceUpdate();
this.props.link.addPoint(point, index);
this.props.pointAdded(point, event);
}
};
generatePoint(pointIndex: number): JSX.Element {
let x = this.props.link.points[pointIndex].x;
let y = this.props.link.points[pointIndex].y;
return (
<g key={"point-" + this.props.link.points[pointIndex].id}>
<circle
cx={x}
cy={y}
r={5}
className={"point pointui" + (this.props.link.points[pointIndex].isSelected() ? " selected" : "")}
/>
<circle
onMouseLeave={() => {
this.setState({ selected: false });
}}
onMouseEnter={() => {
this.setState({ selected: true });
}}
data-id={this.props.link.points[pointIndex].id}
data-linkid={this.props.link.id}
cx={x}
cy={y}
r={15}
opacity={0}
className={"point"}
/>
</g>
);
}
generateLink(extraProps: any, id: string | number): JSX.Element {
var props = this.props;
var Bottom = (
<path
className={this.state.selected || this.props.link.isSelected() ? "selected" : ""}
strokeWidth={props.width}
stroke={props.color}
{...extraProps}
/>
);
var Top = (
<path
strokeLinecap="round"
onMouseLeave={() => {
this.setState({ selected: false });
}}
onMouseEnter={() => {
this.setState({ selected: true });
}}
data-linkid={this.props.link.getID()}
stroke={props.color}
strokeOpacity={this.state.selected ? 0.1 : 0}
strokeWidth={20}
onContextMenu={() => {
if (!this.props.diagramEngine.isModelLocked(this.props.link)) {
event.preventDefault();
this.props.link.remove();
}
}}
{...extraProps}
/>
);
return (
<g key={"link-" + id}>
{Bottom}
{Top}
</g>
);
}
generateLinePath(firstPoint: PointModel, lastPoint: PointModel): string {
return `M${firstPoint.x},${firstPoint.y} L ${lastPoint.x},${lastPoint.y}`;
}
generateCurvePath(
firstPoint: PointModel,
lastPoint: PointModel,
firstPointDelta: number = 0,
lastPointDelta: number = 0
): string {
return `M${firstPoint.x},${firstPoint.y} C ${firstPoint.x + firstPointDelta},${firstPoint.y} ${lastPoint.x +
lastPointDelta},${lastPoint.y} ${lastPoint.x},${lastPoint.y}`;
}
render() {
//ensure id is present for all points on the path
var points = this.props.link.points;
var paths = [];
let model = this.props.diagramEngine.getDiagramModel();
//draw the smoothing
if (points.length === 2) {
//if the points are too close, just draw a straight line
var margin = 50;
if (Math.abs(points[0].x - points[1].x) < 50) {
margin = 5;
}
var pointLeft = points[0];
var pointRight = points[1];
//some defensive programming to make sure the smoothing is
//always in the right direction
if (pointLeft.x > pointRight.x) {
pointLeft = points[1];
pointRight = points[0];
}
paths.push(
this.generateLink(
{
onMouseDown: event => {
this.addPointToLink(event, 1);
},
d: this.generateCurvePath(pointLeft, pointRight, margin, -margin)
},
"0"
)
);
if (this.props.link.targetPort === null) {
paths.push(this.generatePoint(1));
}
} else {
//draw the multiple anchors and complex line instead
var ds = [];
if (this.props.smooth) {
ds.push(this.generateCurvePath(points[0], points[1], 50, 0));
for (var i = 1; i < points.length - 2; i++) {
ds.push(this.generateLinePath(points[i], points[i + 1]));
}
ds.push(this.generateCurvePath(points[i], points[i + 1], 0, -50));
} else {
var ds = [];
for (var i = 0; i < points.length - 1; i++) {
ds.push(this.generateLinePath(points[i], points[i + 1]));
}
}
paths = ds.map((data, index) => {
return this.generateLink(
{
"data-linkid": this.props.link.id,
"data-point": index,
onMouseDown: (event: MouseEvent) => {
this.addPointToLink(event, index + 1);
},
d: data
},
index
);
});
//render the circles
for (var i = 1; i < points.length - 1; i++) {
paths.push(this.generatePoint(i));
}
if (this.props.link.targetPort === null) {
paths.push(this.generatePoint(points.length - 1));
}
}
return <g>{paths}</g>;
}
}

View File

@@ -1,24 +0,0 @@
import { DefaultNodeModel } from "./DefaultNodeModel";
import * as React from "react";
import { DefaultNodeWidget } from "./DefaultNodeWidget";
import { DiagramEngine } from "../DiagramEngine";
import { NodeFactory } from "../AbstractFactory";
/**
* @author Dylan Vorster
*/
export class DefaultNodeFactory extends NodeFactory<DefaultNodeModel> {
constructor() {
super("default");
}
generateReactWidget(diagramEngine: DiagramEngine, node: DefaultNodeModel): JSX.Element {
return React.createElement(DefaultNodeWidget, {
node: node,
diagramEngine: diagramEngine
});
}
getNewInstance(initialConfig?: any): DefaultNodeModel {
return new DefaultNodeModel();
}
}

View File

@@ -1,44 +0,0 @@
import { DefaultPortModel } from "./DefaultPortModel";
import * as _ from "lodash";
import { NodeModel } from "../models/NodeModel";
/**
* @author Dylan Vorster
*/
export class DefaultNodeModel extends NodeModel {
name: string;
color: string;
ports: { [s: string]: DefaultPortModel };
constructor(name: string = "Untitled", color: string = "rgb(0,192,255)") {
super("default");
this.name = name;
this.color = color;
}
deSerialize(object) {
super.deSerialize(object);
this.name = object.name;
this.color = object.color;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
color: this.color
});
}
getInPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return portModel.in;
});
}
getOutPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return !portModel.in;
});
}
}

View File

@@ -1,40 +0,0 @@
import * as React from "react";
import * as _ from "lodash";
import { DefaultNodeModel } from "./DefaultNodeModel";
import { DefaultPortLabel } from "./DefaultPortLabelWidget";
import { DiagramEngine } from "../DiagramEngine";
export interface DefaultNodeProps {
node: DefaultNodeModel;
diagramEngine: DiagramEngine;
}
export interface DefaultNodeState {}
/**
* @author Dylan Vorster
*/
export class DefaultNodeWidget extends React.Component<DefaultNodeProps, DefaultNodeState> {
constructor(props: DefaultNodeProps) {
super(props);
this.state = {};
}
generatePort(port) {
return <DefaultPortLabel model={port} key={port.id} />;
}
render() {
return (
<div className="basic-node" style={{ background: this.props.node.color }}>
<div className="title">
<div className="name">{this.props.node.name}</div>
</div>
<div className="ports">
<div className="in">{_.map(this.props.node.getInPorts(), this.generatePort.bind(this))}</div>
<div className="out">{_.map(this.props.node.getOutPorts(), this.generatePort.bind(this))}</div>
</div>
</div>
);
}
}

View File

@@ -1,12 +0,0 @@
import { DefaultPortModel } from "./DefaultPortModel";
import { PortFactory } from "../AbstractFactory";
export class DefaultPortFactory extends PortFactory<DefaultPortModel> {
constructor() {
super("default");
}
getNewInstance(initialConfig?: any): DefaultPortModel {
return new DefaultPortModel(true, "unknown");
}
}

View File

@@ -1,33 +0,0 @@
import * as React from "react";
import { DefaultPortModel } from "./DefaultPortModel";
import { PortWidget } from "../widgets/PortWidget";
export interface DefaultPortLabelProps {
model?: DefaultPortModel;
in?: boolean;
label?: string;
}
export interface DefaultPortLabelState {}
/**
* @author Dylan Vorster
*/
export class DefaultPortLabel extends React.Component<DefaultPortLabelProps, DefaultPortLabelState> {
public static defaultProps: DefaultPortLabelProps = {
in: true,
label: "port"
};
render() {
var port = <PortWidget node={this.props.model.getParent()} name={this.props.model.name} />;
var label = <div className="name">{this.props.model.label}</div>;
return (
<div className={(this.props.model.in ? "in" : "out") + "-port"}>
{this.props.model.in ? port : label}
{this.props.model.in ? label : port}
</div>
);
}
}

View File

@@ -1,29 +0,0 @@
import * as _ from "lodash";
import { PortModel } from "../models/PortModel";
/**
* @author Dylan Vorster
*/
export class DefaultPortModel extends PortModel {
in: boolean;
label: string;
constructor(isInput: boolean, name: string, label: string = null, id?: string) {
super(name, "default", id);
this.in = isInput;
this.label = label || name;
}
deSerialize(object) {
super.deSerialize(object);
this.in = object.in;
this.label = object.label;
}
serialize() {
return _.merge(super.serialize(), {
in: this.in,
label: this.label
});
}
}

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import { DiagramEngine } from "../../DiagramEngine";
import { DefaultLabelModel } from "../models/DefaultLabelModel";
import { DefaultLabelWidget } from "../widgets/DefaultLabelWidget";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
export class DefaultLabelFactory extends AbstractElementFactory<DefaultLabelModel> {
constructor() {
super("default");
}
generateWidget(engine: DiagramEngine, model: DefaultLabelModel): JSX.Element {
return <DefaultLabelWidget model={model} />;
}
generateModel(): DefaultLabelModel {
return new DefaultLabelModel();
}
}

View File

@@ -0,0 +1,33 @@
import * as React from "react";
import { DefaultLinkWidget } from "../widgets/DefaultLinkWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
import { DefaultLinkModel } from "../models/DefaultLinkModel";
export class DefaultLinkFactory extends AbstractElementFactory<DefaultLinkModel> {
constructor() {
super("default");
}
generateWidget(engine: DiagramEngine, model: DefaultLinkModel): JSX.Element {
return React.createElement(DefaultLinkWidget, {
link: model,
diagramEngine: engine
});
}
generateModel(): DefaultLinkModel {
return new DefaultLinkModel();
}
generateLinkSegment(model: DefaultLinkModel, widget: DefaultLinkWidget, selected: boolean, path: string) {
return (
<path
className={selected ? widget.bem("--path-selected") : ""}
strokeWidth={model.getWidth()}
stroke={model.getColor()}
d={path}
/>
);
}
}

View File

@@ -0,0 +1,22 @@
import { DefaultNodeModel } from "../models/DefaultNodeModel";
import * as React from "react";
import { DefaultNodeWidget } from "../widgets/DefaultNodeWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
export class DefaultNodeFactory extends AbstractElementFactory<DefaultNodeModel> {
constructor() {
super("default");
}
generateWidget(diagramEngine: DiagramEngine, model: DefaultNodeModel): JSX.Element {
return React.createElement(DefaultNodeWidget, {
node: model,
diagramEngine: diagramEngine
});
}
generateModel(): DefaultNodeModel {
return new DefaultNodeModel();
}
}

View File

@@ -0,0 +1,17 @@
import { DefaultPortModel } from "../models/DefaultPortModel";
import { AbstractElementFactory } from "@projectstorm/react-canvas";
import { DiagramEngine } from "storm-react-diagrams";
export class DefaultPortFactory extends AbstractElementFactory<DefaultPortModel> {
constructor() {
super("default");
}
generateWidget(engine: DiagramEngine, model: DefaultPortModel): JSX.Element {
return null;
}
generateModel(): DefaultPortModel {
return new DefaultPortModel(true, "unknown");
}
}

View File

@@ -0,0 +1,27 @@
import { LabelModel } from "../../models/LabelModel";
import * as _ from "lodash";
import { DeserializeEvent } from "@projectstorm/react-canvas";
export class DefaultLabelModel extends LabelModel {
protected label: string;
constructor() {
super("default");
this.offsetY = -23;
}
setLabel(label: string) {
this.label = label;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.label = event.data.label;
}
serialize() {
return _.merge(super.serialize(), {
label: this.label
});
}
}

View File

@@ -0,0 +1,78 @@
import { LinkModel, LinkModelListener } from "../../models/LinkModel";
import * as _ from "lodash";
import { DefaultLabelModel } from "./DefaultLabelModel";
import { LabelModel } from "../../models/LabelModel";
import { BaseEvent, DeserializeEvent } from "@projectstorm/react-canvas";
export interface DefaultLinkModelListener extends LinkModelListener {
colorChanged?(event: BaseEvent<DefaultLinkModel> & { color: null | string }): void;
widthChanged?(event: BaseEvent<DefaultLinkModel> & { width: 0 | number }): void;
}
export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
protected width: number;
protected color: string;
protected curvyness: number;
constructor(type: string = "default") {
super(type);
this.color = "rgba(255,255,255,0.5)";
this.width = 3;
this.curvyness = 50;
}
serialize() {
return _.merge(super.serialize(), {
width: this.width,
color: this.color,
curvyness: this.curvyness
});
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.color = event.data.color;
this.width = event.data.width;
this.curvyness = event.data.curvyness;
}
addLabel(label: LabelModel | string) {
if (label instanceof LabelModel) {
return super.addLabel(label);
}
let labelOb = new DefaultLabelModel();
labelOb.setLabel(label);
return super.addLabel(labelOb);
}
setWidth(width: number) {
this.width = width;
this.iterateListeners("width changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
if (listener.widthChanged) {
listener.widthChanged({ ...event, width: width });
}
});
}
setColor(color: string) {
this.color = color;
this.iterateListeners("color changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
if (listener.colorChanged) {
listener.colorChanged({ ...event, color: color });
}
});
}
getWidth() {
return this.width;
}
getColor() {
return this.color;
}
getCurvyness() {
return this.curvyness;
}
}

View File

@@ -0,0 +1,49 @@
import * as _ from "lodash";
import { DefaultPortModel } from "./DefaultPortModel";
import { NodeModel } from "../../models/NodeModel";
import { Toolkit } from "../../Toolkit";
import { DeserializeEvent } from "@projectstorm/react-canvas";
export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
protected name: string;
protected color: string;
constructor(name: string = "Untitled", color: string = "rgb(0,192,255)") {
super("default");
this.name = name;
this.color = color;
}
addInPort(label: string): DefaultPortModel {
return this.addPort(new DefaultPortModel(true, Toolkit.UID(), label));
}
addOutPort(label: string): DefaultPortModel {
return this.addPort(new DefaultPortModel(false, Toolkit.UID(), label));
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.color = event.data.color;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
color: this.color
});
}
getInPorts(): DefaultPortModel[] {
return _.filter(this.getPorts(), portModel => {
return portModel.in;
});
}
getOutPorts(): DefaultPortModel[] {
return _.filter(this.getPorts(), portModel => {
return !portModel.in;
});
}
}

View File

@@ -0,0 +1,49 @@
import * as _ from "lodash";
import { PortModel } from "../../models/PortModel";
import { DefaultLinkModel } from "./DefaultLinkModel";
import { LinkModel } from "../../models/LinkModel";
import { DeserializeEvent } from "@projectstorm/react-canvas";
export class DefaultPortModel extends PortModel {
in: boolean;
label: string;
links: { [id: string]: DefaultLinkModel };
constructor(isInput: boolean, name: string, label: string = null) {
super(name, "default");
this.in = isInput;
this.label = label || name;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.in = event.data.in;
this.label = event.data.label;
}
serialize() {
return _.merge(super.serialize(), {
in: this.in,
label: this.label
});
}
link(port: PortModel): LinkModel {
let link = this.createLinkModel();
link.setSourcePort(this);
link.setTargetPort(port);
return link;
}
canLinkToPort(port: PortModel): boolean {
if (port instanceof DefaultPortModel) {
return this.in !== port.in;
}
return true;
}
createLinkModel(): LinkModel {
let link = super.createLinkModel();
return link || new DefaultLinkModel();
}
}

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { DefaultLabelModel } from "../models/DefaultLabelModel";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultLabelWidgetProps extends BaseWidgetProps {
model: DefaultLabelModel;
}
export class DefaultLabelWidget extends BaseWidget<DefaultLabelWidgetProps> {
constructor(props) {
super("srd-default-label", props);
}
render() {
return <div {...this.getProps()}>{this.props.model.label}</div>;
}
}

View File

@@ -0,0 +1,388 @@
import * as React from "react";
import { DiagramEngine } from "../../DiagramEngine";
import { PointModel } from "../../models/PointModel";
import { Toolkit } from "../../Toolkit";
import { DefaultLinkFactory } from "../factories/DefaultLinkFactory";
import { DefaultLinkModel } from "../models/DefaultLinkModel";
import PathFinding from "../../routing/PathFinding";
import * as _ from "lodash";
import { LabelModel } from "../../models/LabelModel";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultLinkProps extends BaseWidgetProps {
color?: string;
width?: number;
smooth?: boolean;
link: DefaultLinkModel;
diagramEngine: DiagramEngine;
pointAdded?: (point: PointModel, event: MouseEvent) => any;
}
export interface DefaultLinkState {
selected: boolean;
}
export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkState> {
public static defaultProps: DefaultLinkProps = {
color: "black",
width: 3,
link: null,
engine: null,
smooth: false,
diagramEngine: null
};
// DOM references to the label and paths (if label is given), used to calculate dynamic positioning
refLabels: { [id: string]: HTMLElement };
refPaths: SVGPathElement[];
pathFinding: PathFinding; // only set when smart routing is active
constructor(props: DefaultLinkProps) {
super("srd-default-link", props);
this.refLabels = {};
this.refPaths = [];
this.state = {
selected: false
};
if (props.diagramEngine.isSmartRoutingEnabled()) {
this.pathFinding = new PathFinding(this.props.diagramEngine);
}
}
calculateAllLabelPosition() {
_.forEach(this.props.link.labels, (label, index) => {
this.calculateLabelPosition(label, index + 1);
});
}
componentDidUpdate() {
if (this.props.link.labels.length > 0) {
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
}
}
componentDidMount() {
if (this.props.link.labels.length > 0) {
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
}
}
addPointToLink(event: MouseEvent, index: number) {
if (
!event.shiftKey &&
!this.props.diagramEngine.isModelLocked(this.props.link) &&
this.props.link.points.length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink()
) {
const point = new PointModel(this.props.link, this.props.diagramEngine.getRelativeMousePoint(event));
point.setSelected(true);
this.forceUpdate();
this.props.link.addPoint(point, index);
this.props.pointAdded(point, event);
}
}
generatePoint(pointIndex: number): JSX.Element {
let x = this.props.link.points[pointIndex].getPoint().x;
let y = this.props.link.points[pointIndex].getPoint().y;
return (
<g key={"point-" + this.props.link.points[pointIndex].getID()}>
<circle
cx={x}
cy={y}
r={5}
className={
"point " +
this.bem("__point") +
(this.props.link.points[pointIndex].isSelected() ? this.bem("--point-selected") : "")
}
/>
<circle
onMouseLeave={() => {
this.setState({ selected: false });
}}
onMouseEnter={() => {
this.setState({ selected: true });
}}
data-id={this.props.link.points[pointIndex].getID()}
data-linkid={this.props.link.getID()}
cx={x}
cy={y}
r={15}
opacity={0}
className={"point " + this.bem("__point")}
/>
</g>
);
}
generateLabel(label: LabelModel) {
const canvas = this.props.diagramEngine.canvas as HTMLElement;
return (
<foreignObject
key={label.getID()}
className={this.bem("__label")}
width={canvas.offsetWidth}
height={canvas.offsetHeight}
>
<div ref={ref => (this.refLabels[label.getID()] = ref)}>
{this.props.diagramEngine
.getFactoryForElement(label)
.generateWidget(this.props.diagramEngine, label)}
</div>
</foreignObject>
);
}
generateLink(path: string, extraProps: any, id: string | number): JSX.Element {
var props = this.props;
var Bottom = React.cloneElement(
(props.diagramEngine.getFactoryForElement(this.props.link) as DefaultLinkFactory).generateLinkSegment(
this.props.link,
this,
this.state.selected || this.props.link.isSelected(),
path
),
{
ref: ref => ref && this.refPaths.push(ref)
}
);
var Top = React.cloneElement(Bottom, {
...extraProps,
strokeLinecap: "round",
onMouseLeave: () => {
this.setState({ selected: false });
},
onMouseEnter: () => {
this.setState({ selected: true });
},
ref: null,
"data-linkid": this.props.link.getID(),
strokeOpacity: this.state.selected ? 0.1 : 0,
strokeWidth: 20,
onContextMenu: () => {
if (!this.props.diagramEngine.isModelLocked(this.props.link)) {
event.preventDefault();
this.props.link.remove();
}
}
});
return (
<g key={"link-" + id}>
{Bottom}
{Top}
</g>
);
}
findPathAndRelativePositionToRenderLabel = (index: number): { path: any; position: number } => {
// an array to hold all path lengths, making sure we hit the DOM only once to fetch this information
const lengths = this.refPaths.map(path => path.getTotalLength());
// calculate the point where we want to display the label
let labelPosition =
lengths.reduce((previousValue, currentValue) => previousValue + currentValue, 0) *
(index / (this.props.link.labels.length + 1));
// find the path where the label will be rendered and calculate the relative position
let pathIndex = 0;
while (pathIndex < this.refPaths.length) {
if (labelPosition - lengths[pathIndex] < 0) {
return {
path: this.refPaths[pathIndex],
position: labelPosition
};
}
// keep searching
labelPosition -= lengths[pathIndex];
pathIndex++;
}
};
calculateLabelPosition = (label, index: number) => {
if (!this.refLabels[label.id]) {
// no label? nothing to do here
return;
}
const { path, position } = this.findPathAndRelativePositionToRenderLabel(index);
const labelDimensions = {
width: this.refLabels[label.id].offsetWidth,
height: this.refLabels[label.id].offsetHeight
};
const pathCentre = path.getPointAtLength(position);
const labelCoordinates = {
x: pathCentre.x - labelDimensions.width / 2 + label.offsetX,
y: pathCentre.y - labelDimensions.height / 2 + label.offsetY
};
this.refLabels[label.id].setAttribute(
"style",
`transform: translate(${labelCoordinates.x}px, ${labelCoordinates.y}px);`
);
};
/**
* Smart routing is only applicable when all conditions below are true:
* - smart routing is set to true on the engine
* - current link is between two nodes (not between a node and an empty point)
* - no custom points exist along the line
*/
isSmartRoutingApplicable(): boolean {
const { diagramEngine, link } = this.props;
if (!diagramEngine.isSmartRoutingEnabled()) {
return false;
}
if (link.points.length !== 2) {
return false;
}
if (link.sourcePort === null || link.targetPort === null) {
return false;
}
return true;
}
render() {
const { diagramEngine } = this.props;
if (!diagramEngine.nodesRendered) {
return null;
}
//ensure id is present for all points on the path
var points = this.props.link.points;
var paths = [];
if (this.isSmartRoutingApplicable()) {
// first step: calculate a direct path between the points being linked
const directPathCoords = this.pathFinding.calculateDirectPath(
_.first(points).getPoint(),
_.last(points).getPoint()
);
const routingMatrix = diagramEngine.getRoutingMatrix();
// now we need to extract, from the routing matrix, the very first walkable points
// so they can be used as origin and destination of the link to be created
const smartLink = this.pathFinding.calculateLinkStartEndCoords(routingMatrix, directPathCoords);
if (smartLink) {
const { start, end, pathToStart, pathToEnd } = smartLink;
// second step: calculate a path avoiding hitting other elements
const simplifiedPath = this.pathFinding.calculateDynamicPath(
routingMatrix,
start,
end,
pathToStart,
pathToEnd
);
paths.push(
//smooth: boolean, extraProps: any, id: string | number, firstPoint: PointModel, lastPoint: PointModel
this.generateLink(
Toolkit.generateDynamicPath(simplifiedPath),
{
onMouseDown: event => {
this.addPointToLink(event, 1);
}
},
"0"
)
);
}
}
// true when smart routing was skipped or not enabled.
// See @link{#isSmartRoutingApplicable()}.
if (paths.length === 0) {
if (points.length === 2) {
var isHorizontal =
Math.abs(points[0].getPoint().x - points[1].getPoint().x) >
Math.abs(points[0].getPoint().x - points[1].getPoint().y);
var xOrY = isHorizontal ? "x" : "y";
//draw the smoothing
//if the points are too close, just draw a straight line
var margin = 50;
if (Math.abs(points[0][xOrY] - points[1][xOrY]) < 50) {
margin = 5;
}
var pointLeft = points[0];
var pointRight = points[1];
//some defensive programming to make sure the smoothing is
//always in the right direction
if (pointLeft[xOrY] > pointRight[xOrY]) {
pointLeft = points[1];
pointRight = points[0];
}
paths.push(
this.generateLink(
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.getCurvyness()),
{
onMouseDown: event => {
this.addPointToLink(event, 1);
}
},
"0"
)
);
// draw the link as dangeling
if (this.props.link.targetPort === null) {
paths.push(this.generatePoint(1));
}
} else {
//draw the multiple anchors and complex line instead
for (let j = 0; j < points.length - 1; j++) {
paths.push(
this.generateLink(
Toolkit.generateLinePath(points[j], points[j + 1]),
{
"data-linkid": this.props.link.getID(),
"data-point": j,
onMouseDown: (event: MouseEvent) => {
this.addPointToLink(event, j + 1);
}
},
j
)
);
}
//render the circles
for (var i = 1; i < points.length - 1; i++) {
paths.push(this.generatePoint(i));
}
if (this.props.link.targetPort === null) {
paths.push(this.generatePoint(points.length - 1));
}
}
}
this.refPaths = [];
return (
<g {...this.getProps()}>
{paths}
{_.map(this.props.link.labels, labelModel => {
return this.generateLabel(labelModel);
})}
</g>
);
}
}

View File

@@ -0,0 +1,45 @@
import * as React from "react";
import * as _ from "lodash";
import { DefaultNodeModel } from "../models/DefaultNodeModel";
import { DefaultPortLabel } from "./DefaultPortLabelWidget";
import { DiagramEngine } from "../../DiagramEngine";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultNodeProps extends BaseWidgetProps {
node: DefaultNodeModel;
diagramEngine: DiagramEngine;
}
export interface DefaultNodeState {}
/**
* @author Dylan Vorster
*/
export class DefaultNodeWidget extends BaseWidget<DefaultNodeProps, DefaultNodeState> {
constructor(props: DefaultNodeProps) {
super("srd-default-node", props);
this.state = {};
}
generatePort(port) {
return <DefaultPortLabel model={port} key={port.id} />;
}
render() {
return (
<div {...this.getProps()} style={{ background: this.props.node.color }}>
<div className={this.bem("__title")}>
<div className={this.bem("__name")}>{this.props.node.name}</div>
</div>
<div className={this.bem("__ports")}>
<div className={this.bem("__in")}>
{_.map(this.props.node.getInPorts(), this.generatePort.bind(this))}
</div>
<div className={this.bem("__out")}>
{_.map(this.props.node.getOutPorts(), this.generatePort.bind(this))}
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,35 @@
import * as React from "react";
import { DefaultPortModel } from "../models/DefaultPortModel";
import { PortWidget } from "../../widgets/PortWidget";
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
export interface DefaultPortLabelProps extends BaseWidgetProps {
model: DefaultPortModel;
}
export interface DefaultPortLabelState {}
/**
* @author Dylan Vorster
*/
export class DefaultPortLabel extends BaseWidget<DefaultPortLabelProps, DefaultPortLabelState> {
constructor(props) {
super("srd-default-port", props);
}
getClassName() {
return super.getClassName() + (this.props.model.in ? this.bem("--in") : this.bem("--out"));
}
render() {
var port = <PortWidget node={this.props.model.getParent()} name={this.props.model.name} />;
var label = <div className="name">{this.props.model.label}</div>;
return (
<div {...this.getProps()}>
{this.props.model.in ? port : label}
{this.props.model.in ? label : port}
</div>
);
}
}

View File

@@ -2,33 +2,32 @@
* @author Dylan Vorster
*/
//export defaults
export * from "./defaults/DefaultLinkFactory";
export * from "./defaults/DefaultLinkWidget";
export * from "./defaults/DefaultNodeFactory";
export * from "./defaults/DefaultNodeWidget";
export * from "./defaults/DefaultNodeModel";
export * from "./defaults/DefaultPortModel";
export * from "./defaults/DefaultPortLabelWidget";
export * from "./AbstractFactory";
export * from "./Toolkit";
export * from "./DiagramEngine";
export * from "./models/DiagramModel";
export * from "./BaseEntity";
export * from "./CanvasActions";
export * from "./models/BaseModel";
export * from "./defaults/models/DefaultNodeModel";
export * from "./defaults/models/DefaultPortModel";
export * from "./defaults/models/DefaultLinkModel";
export * from "./defaults/models/DefaultLabelModel";
export * from "./defaults/factories/DefaultLinkFactory";
export * from "./defaults/factories/DefaultNodeFactory";
export * from "./defaults/factories/DefaultPortFactory";
export * from "./defaults/factories/DefaultLabelFactory";
export * from "./defaults/widgets/DefaultLinkWidget";
export * from "./defaults/widgets/DefaultLabelWidget";
export * from "./defaults/widgets/DefaultNodeWidget";
export * from "./defaults/widgets/DefaultPortLabelWidget";
export * from "./routing/PathFinding";
export * from "./models/DiagramModel";
export * from "./models/LinkModel";
export * from "./models/NodeModel";
export * from "./models/PointModel";
export * from "./models/PortModel";
export * from "./models/LabelModel";
export * from "./widgets/DiagramWidget";
export * from "./widgets/LinkLayerWidget";
export * from "./widgets/LinkWidget";
export * from "./widgets/NodeLayerWidget";
export * from "./widgets/NodeWidget";
export * from "./widgets/PortWidget";

View File

@@ -1,72 +0,0 @@
import { BaseEntity, BaseListener } from "../BaseEntity";
import * as _ from "lodash";
import { BaseEvent } from "../BaseEntity";
export interface BaseModelListener extends BaseListener {
selectionChanged?(event: BaseEvent<BaseModel> & { isSelected: boolean }): void;
entityRemoved?(event: BaseEvent<BaseModel>): void;
}
/**
* @author Dylan Vorster
*/
export class BaseModel<T extends BaseModelListener = BaseModelListener> extends BaseEntity<BaseModelListener> {
type: string;
selected: boolean;
constructor(type?: string, id?: string) {
super(id);
this.type = type;
this.selected = false;
}
public getSelectedEntities(): BaseModel<T>[] {
if (this.isSelected()) {
return [this];
}
return [];
}
public deSerialize(ob) {
super.deSerialize(ob);
this.type = ob.type;
this.selected = ob.selected;
}
public serialize() {
return _.merge(super.serialize(), {
type: this.type,
selected: this.selected
});
}
public getType(): string {
return this.type;
}
public getID(): string {
return this.id;
}
public isSelected(): boolean {
return this.selected;
}
public setSelected(selected: boolean = true) {
this.selected = selected;
this.iterateListeners((listener, event) => {
if (listener.selectionChanged) {
listener.selectionChanged({ ...event, isSelected: selected });
}
});
}
public remove() {
this.iterateListeners((listener, event) => {
if (listener.entityRemoved) {
listener.entityRemoved(event);
}
});
}
}

View File

@@ -1,293 +1,104 @@
import { BaseListener, BaseEntity, BaseEvent, BaseEntityType } from "../BaseEntity";
import * as _ from "lodash";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "./LinkModel";
import { NodeModel } from "./NodeModel";
import { PortModel } from "./PortModel";
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PointModel } from "./PointModel";
/**
* @author Dylan Vorster
*
*/
export interface DiagramListener extends BaseListener {
import { BaseModel, BaseEvent, CanvasModel, CanvasModelListener, CanvasLayerModel } from "@projectstorm/react-canvas";
export interface DiagramListener extends CanvasModelListener {
nodesUpdated?(event: BaseEvent & { node: NodeModel; isCreated: boolean }): void;
linksUpdated?(event: BaseEvent & { link: LinkModel; isCreated: boolean }): void;
offsetUpdated?(event: BaseEvent<DiagramModel> & { offsetX: number; offsetY: number }): void;
zoomUpdated?(event: BaseEvent<DiagramModel> & { zoom: number }): void;
gridUpdated?(event: BaseEvent<DiagramModel> & { size: number }): void;
}
/**
*
*/
export class DiagramModel extends BaseEntity<DiagramListener> {
//models
links: { [s: string]: LinkModel };
nodes: { [s: string]: NodeModel };
//control variables
offsetX: number;
offsetY: number;
zoom: number;
rendered: boolean;
gridSize: number;
export class DiagramModel extends CanvasModel<DiagramListener> {
linksLayer: CanvasLayerModel<LinkModel>;
nodesLayer: CanvasLayerModel<NodeModel>;
constructor() {
super();
this.links = {};
this.nodes = {};
this.linksLayer = new CanvasLayerModel();
this.nodesLayer = new CanvasLayerModel();
this.offsetX = 0;
this.offsetY = 0;
this.zoom = 100;
this.rendered = false;
this.gridSize = 0;
}
this.linksLayer.setSVG(true);
this.linksLayer.setTransformable(true);
setGridSize(size: number = 0) {
this.gridSize = size;
this.iterateListeners((listener, event) => {
listener.gridUpdated && listener.gridUpdated({ ...event, size: size });
});
}
this.nodesLayer.setSVG(false);
this.nodesLayer.setTransformable(true);
getGridPosition(pos) {
if (this.gridSize === 0) {
return pos;
}
return this.gridSize * Math.floor((pos + this.gridSize / 2) / this.gridSize);
}
deSerializeDiagram(object: any, diagramEngine: DiagramEngine) {
this.deSerialize(object);
this.offsetX = object.offsetX;
this.offsetY = object.offsetY;
this.zoom = object.zoom;
this.gridSize = object.gridSize;
//deserialize nodes
_.forEach(object.nodes, (node: any) => {
let nodeOb = diagramEngine.getNodeFactory(node.type).getNewInstance(node);
nodeOb.deSerialize(node);
//deserialize ports
_.forEach(node.ports, (port: any) => {
let portOb = diagramEngine.getPortFactory(port.type).getNewInstance();
portOb.deSerialize(port);
nodeOb.addPort(portOb);
});
this.addNode(nodeOb);
});
_.forEach(object.links, (link: any) => {
let linkOb = diagramEngine.getLinkFactory(link.type).getNewInstance();
linkOb.deSerialize(link);
if (link.target) {
linkOb.setTargetPort(this.getNode(link.target).getPortFromID(link.targetPort));
}
if (link.source) {
linkOb.setSourcePort(this.getNode(link.source).getPortFromID(link.sourcePort));
}
this.addLink(linkOb);
});
}
serializeDiagram() {
return _.merge(this.serialize(), {
offsetX: this.offsetX,
offsetY: this.offsetY,
zoom: this.zoom,
gridSize: this.gridSize,
links: _.map(this.links, link => {
return link.serialize();
}),
nodes: _.map(this.nodes, link => {
return link.serialize();
})
});
}
clearSelection(ignore: BaseModel<BaseModelListener> | null = null) {
_.forEach(this.getSelectedItems(), element => {
if (ignore && ignore.getID() === element.getID()) {
return;
}
element.setSelected(false); //TODO dont fire the listener
});
}
getSelectedItems(...filters: BaseEntityType[]): BaseModel<BaseModelListener>[] {
if (!Array.isArray(filters)) {
filters = [filters];
}
var items = [];
// run through nodes
items = items.concat(
_.flatMap(this.nodes, node => {
return node.getSelectedEntities();
})
);
// find all the links
items = items.concat(
_.flatMap(this.links, link => {
return link.getSelectedEntities();
})
);
//find all points
items = items.concat(
_.flatMap(this.links, link => {
return _.flatMap(link.points, point => {
return point.getSelectedEntities();
});
})
);
items = _.uniq(items);
if (filters.length > 0) {
items = _.filter(_.uniq(items), (item: BaseModel<any>) => {
if (_.includes(filters, "node") && item instanceof NodeModel) {
return true;
}
if (_.includes(filters, "link") && item instanceof LinkModel) {
return true;
}
if (_.includes(filters, "port") && item instanceof PortModel) {
return true;
}
if (_.includes(filters, "point") && item instanceof PointModel) {
return true;
}
return false;
});
}
return items;
}
setZoomLevel(zoom: number) {
this.zoom = zoom;
this.iterateListeners((listener, event) => {
listener.zoomUpdated && listener.zoomUpdated({ ...event, zoom: zoom });
});
}
setOffset(offsetX: number, offsetY: number) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.iterateListeners((listener, event) => {
listener.offsetUpdated && listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: offsetY });
});
}
setOffsetX(offsetX: number) {
this.offsetX = offsetX;
this.iterateListeners((listener, event) => {
listener.offsetUpdated && listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: this.offsetY });
});
}
setOffsetY(offsetY: number) {
this.offsetY = offsetY;
this.iterateListeners((listener, event) => {
listener.offsetUpdated &&
listener.offsetUpdated({ ...event, offsetX: this.offsetX, offsetY: this.offsetY });
});
}
getOffsetY() {
return this.offsetY;
}
getOffsetX() {
return this.offsetX;
}
getZoomLevel() {
return this.zoom;
this.addLayer(this.linksLayer);
this.addLayer(this.nodesLayer);
}
getNode(node: string | NodeModel): NodeModel | null {
if (node instanceof NodeModel) {
return node;
}
if (!this.nodes[node]) {
return null;
}
return this.nodes[node];
return this.nodesLayer.getEntity[node] || null;
}
getLink(link: string | LinkModel): LinkModel | null {
if (link instanceof LinkModel) {
return link;
}
if (!this.links[link]) {
return null;
}
return this.links[link];
return this.linksLayer.getEntity(link) || null;
}
addAll(...models: BaseModel[]): BaseModel[] {
_.forEach(models, model => {
if (model instanceof LinkModel) {
this.addLink(model);
} else if (model instanceof NodeModel) {
this.addNode(model);
}
});
return models;
}
addLink(link: LinkModel): LinkModel {
link.addListener({
entityRemoved: () => {
this.removeLink(link);
this.linksLayer.addEntity(link);
this.iterateListeners("link added", (listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link, isCreated: true });
}
});
this.links[link.getID()] = link;
this.iterateListeners((listener, event) => {
listener.linksUpdated && listener.linksUpdated({ ...event, link: link, isCreated: true });
});
return link;
}
addNode(node: NodeModel): NodeModel {
node.addListener({
entityRemoved: () => {
this.removeNode(node);
this.nodesLayer.addEntity(node);
this.iterateListeners("node added", (listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node, isCreated: true });
}
});
this.nodes[node.getID()] = node;
this.iterateListeners((listener, event) => {
listener.nodesUpdated && listener.nodesUpdated({ ...event, node: node, isCreated: true });
});
return node;
}
removeLink(link: LinkModel | string) {
link = this.getLink(link);
delete this.links[link.getID()];
this.iterateListeners((listener, event) => {
listener.linksUpdated && listener.linksUpdated({ ...event, link: link as LinkModel, isCreated: false });
this.linksLayer.removeEntity(link);
this.iterateListeners("link removed", (listener, event) => {
if (listener.linksUpdated) {
listener.linksUpdated({ ...event, link: link as LinkModel, isCreated: false });
}
});
}
removeNode(node: NodeModel | string) {
node = this.getNode(node);
delete this.nodes[node.getID()];
this.iterateListeners((listener, event) => {
listener.nodesUpdated && listener.nodesUpdated({ ...event, node: node as NodeModel, isCreated: false });
this.nodesLayer.removeEntity(node);
this.iterateListeners("node removed", (listener, event) => {
if (listener.nodesUpdated) {
listener.nodesUpdated({ ...event, node: node as NodeModel, isCreated: false });
}
});
}
getLinks(): { [s: string]: LinkModel } {
return this.links;
return this.linksLayer.getEntities();
}
getNodes(): { [s: string]: NodeModel } {
return this.nodes;
return this.nodesLayer.getEntities();
}
}

27
src/models/LabelModel.ts Normal file
View File

@@ -0,0 +1,27 @@
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import { BaseModel, DeserializeEvent } from "@projectstorm/react-canvas";
export class LabelModel extends BaseModel<LinkModel> {
offsetX: number;
offsetY: number;
constructor(type?: string) {
super(type);
this.offsetX = 0;
this.offsetY = 0;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.offsetX = event.data.offsetX;
this.offsetY = event.data.offsetY;
}
serialize() {
return _.merge(super.serialize(), {
offsetX: this.offsetX,
offsetY: this.offsetY
});
}
}

View File

@@ -1,66 +1,71 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PortModel } from "./PortModel";
import { PointModel } from "./PointModel";
import * as _ from "lodash";
import { BaseEvent } from "../BaseEntity";
import { LabelModel } from "./LabelModel";
import {
BaseEvent,
GraphModel,
GraphModelOrdered,
CanvasElementModel,
CanvasElementModelListener,
Rectangle,
DeserializeEvent
} from "@projectstorm/react-canvas";
export interface LinkModelListener extends BaseModelListener {
sourcePortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
export interface LinkModelListener<T extends LinkModel = any> extends CanvasElementModelListener<T> {
sourcePortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
}
export class LinkModel extends BaseModel<LinkModelListener> {
sourcePort: PortModel | null;
targetPort: PortModel | null;
points: PointModel[];
extras: {};
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends CanvasElementModel<T> {
protected sourcePort: PortModel | null;
protected targetPort: PortModel | null;
protected labels: GraphModel<LabelModel, LinkModel>;
protected points: GraphModelOrdered<PointModel, LinkModel>;
constructor(linkType: string = "default", id?: string) {
super(linkType, id);
this.points = [new PointModel(this, { x: 0, y: 0 }), new PointModel(this, { x: 0, y: 0 })];
this.extras = {};
constructor(linkType: string = "default") {
super(linkType);
this.points = new GraphModelOrdered();
this.labels = new GraphModel();
this.points.setParentDelegate(this);
this.labels.setParentDelegate(this);
this.sourcePort = null;
this.targetPort = null;
}
deSerialize(ob) {
super.deSerialize(ob);
this.extras = ob.extras;
this.points = _.map(ob.points, (point: { x; y }) => {
var p = new PointModel(this, { x: point.x, y: point.y });
p.deSerialize(point);
return p;
});
setDimensions(dimensions: Rectangle) {
throw new Error("Method not implemented.");
}
getDimensions(): Rectangle {
throw new Error("Method not implemented.");
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.points.deSerialize(event.subset("points"));
this.labels.deSerialize(event.subset("labels"));
if (event.data.target) {
this.setTargetPort(event.cache[event.data.targetPort] as PortModel);
}
if (event.data.source) {
this.setSourcePort(event.cache[event.data.sourcePort] as PortModel);
}
}
serialize() {
return _.merge(super.serialize(), {
source: this.sourcePort ? this.sourcePort.getParent().id : null,
sourcePort: this.sourcePort ? this.sourcePort.id : null,
target: this.targetPort ? this.targetPort.getParent().id : null,
targetPort: this.targetPort ? this.targetPort.id : null,
points: _.map(this.points, point => {
return point.serialize();
}),
extras: this.extras
source: this.sourcePort ? this.sourcePort.getParent().getID() : null,
sourcePort: this.sourcePort ? this.sourcePort.getID() : null,
target: this.targetPort ? this.targetPort.getParent().getID() : null,
targetPort: this.targetPort ? this.targetPort.getID() : null,
points: this.points.serialize(),
labels: this.labels.serialize()
});
}
doClone(lookupTable = {}, clone) {
clone.setPoints(
_.map(this.getPoints(), (point: PointModel) => {
return point.clone(lookupTable);
})
);
if (this.sourcePort) {
clone.setSourcePort(this.sourcePort.clone(lookupTable));
}
if (this.targetPort) {
clone.setTargetPort(this.targetPort.clone(lookupTable));
}
}
remove() {
if (this.sourcePort) {
this.sourcePort.removeLink(this);
@@ -68,25 +73,19 @@ export class LinkModel extends BaseModel<LinkModelListener> {
if (this.targetPort) {
this.targetPort.removeLink(this);
}
super.remove();
}
isLastPoint(point: PointModel) {
var index = this.getPointIndex(point);
return index === this.points.length - 1;
return index === this.points.count() - 1;
}
getPointIndex(point: PointModel) {
return this.points.indexOf(point);
return _.values(this.points.getEntities()).indexOf(point);
}
getPointModel(id: string): PointModel | null {
for (var i = 0; i < this.points.length; i++) {
if (this.points[i].id === id) {
return this.points[i];
}
}
return null;
return this.points.getEntities()[id];
}
getPortForPoint(point: PointModel): PortModel {
@@ -110,18 +109,25 @@ export class LinkModel extends BaseModel<LinkModelListener> {
}
getFirstPoint(): PointModel {
return this.points[0];
return _.values(this.points.getEntities())[0];
}
getLastPoint(): PointModel {
return this.points[this.points.length - 1];
return _.values(this.points.getEntities())[this.points.count() - 1];
}
setSourcePort(port: PortModel) {
port.addLink(this);
if (port !== null) {
port.addLink(this);
}
if (this.sourcePort !== null) {
this.sourcePort.removeLink(this);
}
this.sourcePort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
listener.sourcePortChanged && listener.sourcePortChanged({ ...event, port: port });
this.iterateListeners("source port changed", (listener: T, event) => {
if (listener.sourcePortChanged) {
listener.sourcePortChanged({ ...event, port: port });
}
});
}
@@ -134,38 +140,53 @@ export class LinkModel extends BaseModel<LinkModelListener> {
}
setTargetPort(port: PortModel) {
port.addLink(this);
if (port !== null) {
port.addLink(this);
}
if (this.targetPort !== null) {
this.targetPort.removeLink(this);
}
this.targetPort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
listener.targetPortChanged && listener.targetPortChanged({ ...event, port: port });
this.iterateListeners("target port chnaged", (listener: T, event) => {
if (listener.targetPortChanged) {
listener.targetPortChanged({ ...event, port: port });
}
});
}
point(x: number, y: number): PointModel {
return this.addPoint(this.generatePoint(x, y));
}
addLabel(label: LabelModel) {
this.labels.addEntity(label);
}
getPoints(): PointModel[] {
return this.points;
return this.points.getArray();
}
setPoints(points: PointModel[]) {
_.forEach(points, point => {
point.link = this;
point.setLink(this);
});
this.points = points;
this.points.addEntities(points);
}
removePoint(pointModel: PointModel) {
this.points.splice(this.getPointIndex(pointModel), 1);
this.points.removeEntity(pointModel);
}
removePointsBefore(pointModel: PointModel) {
this.points.splice(0, this.getPointIndex(pointModel));
addPoint<P extends PointModel>(pointModel: P, index = 1): P {
pointModel.setLink(this);
this.points.addEntity(pointModel, index);
return pointModel;
}
removePointsAfter(pointModel: PointModel) {
this.points.splice(this.getPointIndex(pointModel) + 1);
}
addPoint(pointModel: PointModel, index = 1) {
pointModel.link = this;
this.points.splice(index, 0, pointModel);
generatePoint(x: number, y: number): PointModel {
let point = new PointModel(this);
point.getPoint().x = x;
point.getPoint().y = y;
return point;
}
}

View File

@@ -1,116 +1,76 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { PortModel } from "./PortModel";
import * as _ from "lodash";
import { Rectangle, CanvasElementModel, GraphModel, DeserializeEvent } from "@projectstorm/react-canvas";
export class NodeModel extends BaseModel<BaseModelListener> {
x: number;
y: number;
extras: {};
ports: { [s: string]: PortModel };
export class NodeModel<T extends PortModel = PortModel> extends CanvasElementModel {
protected dimensions: Rectangle;
protected ports: GraphModel<T, null>;
constructor(nodeType: string = "default", id?: string) {
super(nodeType, id);
this.x = 0;
this.y = 0;
this.extras = {};
this.ports = {};
constructor(nodeType: string = "default") {
super(nodeType);
this.dimensions = new Rectangle(0, 0, 0, 0);
this.ports = new GraphModel("ports");
}
setPosition(x, y) {
//store position
let oldX = this.x;
let oldY = this.y;
for (let port in this.ports) {
_.forEach(this.ports[port].getLinks(), link => {
let point = link.getPointForPort(this.ports[port]);
point.x = point.x + x - oldX;
point.y = point.y + y - oldY;
});
}
this.x = x;
this.y = y;
setDimensions(dimensions: Rectangle) {
this.dimensions = dimensions;
}
getSelectedEntities() {
let entities = super.getSelectedEntities();
// add the points of each link that are selected here
if (this.isSelected()) {
for (let portName in this.ports) {
entities = entities.concat(
_.map(this.ports[portName].getLinks(), link => {
return link.getPointForPort(this.ports[portName]);
})
);
}
}
return entities;
getDimensions(): Rectangle {
return this.dimensions;
}
deSerialize(ob) {
super.deSerialize(ob);
this.x = ob.x;
this.y = ob.y;
this.extras = ob.extras;
setPosition(x:number, y:number){
this.dimensions.updateDimensions(x,y, this.dimensions.getWidth(), this.dimensions.getHeight());
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.dimensions.deserialize(event.data.dimensions);
//deserialize ports
let ports = event.subset("ports");
_.forEach(ports.data, (port: any, index) => {
let portOb = event.engine.getFactory(port.type).generateModel() as T;
portOb.deSerialize(ports.subset(index));
this.addPort(portOb);
});
}
serialize() {
return _.merge(super.serialize(), {
x: this.x,
y: this.y,
extras: this.extras,
ports: _.map(this.ports, port => {
return port.serialize();
})
dimensions: this.dimensions.serialize(),
ports: this.ports.serialize()
});
}
doClone(lookupTable = {}, clone) {
// also clone the ports
clone.ports = {};
_.values(this.ports).forEach(port => {
clone.addPort(port.clone(lookupTable));
});
}
remove() {
super.remove();
for (var i in this.ports) {
_.forEach(this.ports[i].getLinks(), link => {
link.remove();
});
}
}
getPortFromID(id): PortModel | null {
for (var i in this.ports) {
if (this.ports[i].id === id) {
getPortFromID(id): T | null {
for (let i in this.ports) {
if (this.ports[i].getID() === id) {
return this.ports[i];
}
}
return null;
}
getPort(name: string): PortModel | null {
return this.ports[name];
getPort(name: string): T | null {
return this.ports.getEntities()[name];
}
getPorts(): { [s: string]: PortModel } {
return this.ports;
getPorts(): { [s: string]: T } {
return this.ports.getEntities();
}
removePort(port: PortModel) {
removePort(port: T) {
//clear the parent node reference
if (this.ports[port.name]) {
this.ports[port.name].setParentNode(null);
this.ports[port.name].setParent(null);
delete this.ports[port.name];
}
}
addPort(port: PortModel): PortModel {
port.setParentNode(this);
this.ports[port.name] = port;
addPort(port: T): T {
this.ports.addEntity(port);
return port;
}
}

View File

@@ -1,40 +1,51 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
import {
Point,
CanvasElementModel,
CanvasElementModelListener,
Rectangle,
DeserializeEvent
} from "@projectstorm/react-canvas";
export class PointModel extends BaseModel<BaseModelListener> {
x: number;
y: number;
link: LinkModel;
export class PointModel extends CanvasElementModel<CanvasElementModelListener> {
protected point: Point;
protected link: LinkModel;
constructor(link: LinkModel, points: { x: number; y: number }) {
super();
this.x = points.x;
this.y = points.y;
constructor(link: LinkModel) {
super("point");
this.link = link;
}
getSelectedEntities() {
if (super.isSelected() && !this.isConnectedToPort()) {
return [this];
}
return [];
setDimensions(dimensions: Rectangle) {
this.point = dimensions.getTopLeft();
}
getDimensions(): Rectangle {
return new Rectangle(this.point, 10, 10);
}
isConnectedToPort(): boolean {
return this.link.getPortForPoint(this) !== null;
}
deSerialize(ob) {
super.deSerialize(ob);
this.x = ob.x;
this.y = ob.y;
setLink(link: LinkModel) {
this.link = link;
}
getLink(): LinkModel {
return this.link;
}
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.point = new Point(event.data["x"], event.data["y"]);
}
serialize() {
return _.merge(super.serialize(), {
x: this.x,
y: this.y
x: this.point.x,
y: this.point.y
});
}
@@ -43,27 +54,9 @@ export class PointModel extends BaseModel<BaseModelListener> {
if (this.link) {
this.link.removePoint(this);
}
super.remove();
}
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;
}
isLocked() {
return super.isLocked() || this.getLink().isLocked();
getPoint(): Point {
return this.point;
}
}

View File

@@ -1,50 +1,51 @@
import { BaseModel, BaseModelListener } from "./BaseModel";
import { BaseModel, BaseListener, DeserializeEvent } from "@projectstorm/react-canvas";
import { NodeModel } from "./NodeModel";
import { LinkModel } from "./LinkModel";
import * as _ from "lodash";
export class PortModel extends BaseModel<BaseModelListener> {
export class PortModel extends BaseModel<NodeModel, BaseListener> {
name: string;
parentNode: NodeModel;
links: { [id: string]: LinkModel };
maximumLinks: number;
constructor(name: string, type?: string, id?: string) {
super(type, id);
constructor(name: string, type?: string, maximumLinks?: number) {
super(type);
this.name = name;
this.links = {};
this.parentNode = null;
this.maximumLinks = maximumLinks;
}
deSerialize(ob) {
super.deSerialize(ob);
this.name = ob.name;
deSerialize(event: DeserializeEvent) {
super.deSerialize(event);
this.name = event.data.name;
this.maximumLinks = event.data.maximumLinks;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
parentNode: this.parentNode.id,
parentNode: this.parent.getID(),
links: _.map(this.links, link => {
return link.id;
})
return link.getID();
}),
maximumLinks: this.maximumLinks
});
}
doClone(lookupTable = {}, clone) {
clone.links = {};
clone.parentNode = this.parentNode.clone(lookupTable);
getNode(): NodeModel {
return this.getParent();
}
getName(): string {
return this.name;
}
getParent(): NodeModel {
return this.parentNode;
getMaximumLinks(): number {
return this.maximumLinks;
}
setParentNode(node: NodeModel) {
this.parentNode = node;
setMaximumLinks(maximumLinks: number) {
this.maximumLinks = maximumLinks;
}
removeLink(link: LinkModel) {
@@ -59,13 +60,19 @@ export class PortModel extends BaseModel<BaseModelListener> {
return this.links;
}
createLinkModel(): LinkModel | null {
var linkModel = new LinkModel();
linkModel.setSourcePort(this);
return linkModel;
public createLinkModel(): LinkModel | null {
if (_.isFinite(this.maximumLinks)) {
var numberOfLinks: number = _.size(this.links);
if (this.maximumLinks === 1 && numberOfLinks >= 1) {
return _.values(this.links)[0];
} else if (numberOfLinks >= this.maximumLinks) {
return null;
}
}
return null;
}
isLocked() {
return super.isLocked() || this.getParent().isLocked();
canLinkToPort(port: PortModel): boolean {
return true;
}
}

View File

@@ -1,144 +0,0 @@
.storm-diagrams-canvas{
position: relative;
flex-grow: 1;
display: flex;
cursor: move;
overflow: hidden;
.point{
fill: rgba(white,0.5);
&.selected{
fill: rgb(0,192,255);
}
}
.selector{
position: absolute;
background-color: rgba(0,192,255,0.2);
border: solid 2px rgb(0,192,255);
}
svg{
position: absolute;
height: 100%;
width: 100%;
transform-origin: 0 0;
overflow: visible;
}
.node-view{
top:0;
left:0;
right:0;
bottom:0;
position: absolute;
pointer-events: none;
transform-origin: 0 0;
}
.node{
position: absolute;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
user-select: none;
cursor: move;
pointer-events: all;
&.selected{
>* {
border-color:rgb(0,192,255) !important;
//-webkit-filter: drop-shadow( 0 0 20px rgba(0,192,255,0.5));
}
}
}
@keyframes dash {
from {
stroke-dashoffset: 24;
}
to {
stroke-dashoffset: 0;
}
}
path{
fill:none;
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;
font-family:Arial;
color: white;
border: solid 2px black;
overflow: visible;
font-size: 11px;
//box-shadow: 0 0 10px rgba(black,0.5);
.title{
/* background-image: linear-gradient(rgba(black,0.1),rgba(black,0.2));*/
background: rgba(black,0.3);
display: flex;
white-space: nowrap;
>*{
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{
padding: 0 5px;
}
}
.out-port{
justify-content: flex-end;
.name{
justify-content: flex-end;
text-align: right;
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
.srd-diagram{
position: relative;
flex-grow: 1;
display: flex;
cursor: move;
overflow: hidden;
&__selector{
position: absolute;
background-color: rgba(0,192,255,0.2);
border: solid 2px rgb(0,192,255);
}
}

14
src/sass/_NodeWidget.scss Normal file
View File

@@ -0,0 +1,14 @@
.srd-node{
position: absolute;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
user-select: none;
cursor: move;
pointer-events: all;
&--selected{
> * {
border-color:rgb(0,192,255) !important;
}
}
}

View File

@@ -0,0 +1,9 @@
.srd-port{
width: 15px;
height: 15px;
background: rgba(white,0.1);
&:hover,&.selected{
background: rgb(192,255,0);
}
}

View File

@@ -0,0 +1,9 @@
.srd-default-label{
background: rgba(70, 70, 70, 0.8);
border: 1px solid #333;
border-radius: 4px;
color: #fff;
display: inline-block;
font-size: smaller;
padding: 5px;
}

View File

@@ -0,0 +1,39 @@
.srd-default-link{
@keyframes dash {
from {
stroke-dashoffset: 24;
}
to {
stroke-dashoffset: 0;
}
}
path{
fill:none;
pointer-events:all;
}
&--path-selected{
stroke: rgb(0,192,255) !important;
stroke-dasharray: 10,2;
animation: dash 1s linear infinite;
}
&__label {
pointer-events: none;
> div{
display: inline-block;
position: absolute;
}
}
&__point{
fill: rgba(white,0.5);
}
&--point-selected{
fill: rgb(0,192,255);
}
}

View File

@@ -0,0 +1,43 @@
.srd-default-node {
background-color: rgb(30, 30, 30);
border-radius: 5px;
font-family: sans-serif;
color: white;
border: solid 2px black;
overflow: visible;
font-size: 11px;
&__title {
background: rgba(black, 0.3);
display: flex;
white-space: nowrap;
> * {
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;
}
}

View File

@@ -0,0 +1,24 @@
.srd-default-port{
$p: &;
display: flex;
margin-top: 1px;
> * {
align-self: center;
}
&__name {
padding: 0 5px;
}
&--out{
justify-content: flex-end;
#{$p}__name {
justify-content: flex-end;
text-align: right;
}
}
}

9
src/sass/main.scss Normal file
View File

@@ -0,0 +1,9 @@
@import "DiagramWidget";
@import "NodeWidget";
@import "PortWidget";
//defaults
@import "defaults/DefaultNodeWidget";
@import "defaults/DefaultPortWidget";
@import "defaults/DefaultLabelWidget";
@import "defaults/DefaultLinkWidget";

View File

@@ -1,500 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import * as _ from "lodash";
import { LinkLayerWidget } from "./LinkLayerWidget";
import { NodeLayerWidget } from "./NodeLayerWidget";
import { Toolkit } from "../Toolkit";
import { BaseAction, MoveCanvasAction, MoveItemsAction, SelectingAction } from "../CanvasActions";
import { NodeModel } from "../models/NodeModel";
import { PointModel } from "../models/PointModel";
import { PortModel } from "../models/PortModel";
import { LinkModel } from "../models/LinkModel";
import { BaseModel, BaseModelListener } from "../models/BaseModel";
export interface SelectionModel {
model: BaseModel<BaseModelListener>;
initialX: number;
initialY: number;
}
export interface DiagramProps {
diagramEngine: DiagramEngine;
allowLooseLinks?: boolean;
allowCanvasTranslation?: boolean;
allowCanvasZoom?: boolean;
inverseZoom?: boolean;
maxNumberPointsPerLink?: number;
actionStartedFiring?: (action: BaseAction) => boolean;
actionStillFiring?: (action: BaseAction) => void;
actionStoppedFiring?: (action: BaseAction) => void;
deleteKeys?: number[];
}
export interface DiagramState {
action: BaseAction | null;
wasMoved: boolean;
renderedNodes: boolean;
windowListener: any;
diagramEngineListener: any;
document: any;
}
/**
* @author Dylan Vorster
*/
export class DiagramWidget extends React.Component<DiagramProps, DiagramState> {
public static defaultProps: DiagramProps = {
diagramEngine: null,
allowLooseLinks: true,
allowCanvasTranslation: true,
allowCanvasZoom: true,
inverseZoom: false,
maxNumberPointsPerLink: Infinity, // backwards compatible default
deleteKeys: [46, 8]
};
constructor(props: DiagramProps) {
super(props);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.state = {
action: null,
wasMoved: false,
renderedNodes: false,
windowListener: null,
diagramEngineListener: null,
document: null
};
}
onKeyUpPointer: null;
componentWillUnmount() {
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
this.props.diagramEngine.setCanvas(null);
window.removeEventListener("keyup", this.onKeyUpPointer);
window.removeEventListener("mouseUp", this.onMouseUp);
window.removeEventListener("mouseMove", this.onMouseMove);
}
componentWillReceiveProps(nextProps: DiagramProps) {
if (this.props.diagramEngine !== nextProps.diagramEngine) {
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
const diagramEngineListener = nextProps.diagramEngine.addListener({
repaintCanvas: () => this.forceUpdate()
});
this.setState({ diagramEngineListener });
}
}
componentWillUpdate(nextProps: DiagramProps) {
if (this.props.diagramEngine.diagramModel.id !== nextProps.diagramEngine.diagramModel.id) {
this.setState({ renderedNodes: false });
nextProps.diagramEngine.diagramModel.rendered = true;
}
if (!nextProps.diagramEngine.diagramModel.rendered) {
this.setState({ renderedNodes: false });
nextProps.diagramEngine.diagramModel.rendered = true;
}
}
componentDidUpdate() {
if (!this.state.renderedNodes) {
this.setState({
renderedNodes: true
});
}
}
componentDidMount() {
this.onKeyUpPointer = this.onKeyUp.bind(this);
//add a keyboard listener
this.setState({
document: document,
renderedNodes: true,
diagramEngineListener: this.props.diagramEngine.addListener({
repaintCanvas: () => {
this.forceUpdate();
}
})
});
window.addEventListener("keyup", this.onKeyUpPointer, false);
// dont focus the window when in test mode - jsdom fails
if (process.env.NODE_ENV !== "test") {
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 = Toolkit.closest(target, ".port[data-name]");
if (element) {
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 = 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 = Toolkit.closest(target, "[data-linkid]");
if (element) {
return {
model: diagramModel.getLink(element.getAttribute("data-linkid")),
element: element
};
}
//look for a node
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) {
setState = this.props.actionStartedFiring(action);
}
if (setState) {
this.setState({ action: action });
}
}
onMouseMove(event) {
var diagramEngine = this.props.diagramEngine;
var diagramModel = diagramEngine.getDiagramModel();
//select items so draw a bounding box
if (this.state.action instanceof SelectingAction) {
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
_.forEach(diagramModel.getNodes(), node => {
if ((this.state.action as SelectingAction).containsElement(node.x, node.y, diagramModel)) {
node.setSelected(true);
}
});
_.forEach(diagramModel.getLinks(), link => {
var allSelected = true;
_.forEach(link.points, point => {
if ((this.state.action as SelectingAction).containsElement(point.x, point.y, diagramModel)) {
point.setSelected(true);
} else {
allSelected = false;
}
});
if (allSelected) {
link.setSelected(true);
}
});
this.state.action.mouseX2 = relative.x;
this.state.action.mouseY2 = relative.y;
this.fireAction();
this.setState({ action: this.state.action });
return;
} else if (this.state.action instanceof MoveItemsAction) {
let amountX = event.clientX - this.state.action.mouseX;
let amountY = event.clientY - this.state.action.mouseY;
let amountZoom = diagramModel.getZoomLevel() / 100;
_.forEach(this.state.action.selectionModels, model => {
// in this case we need to also work out the relative grid position
if (
model.model instanceof NodeModel ||
(model.model instanceof PointModel && !model.model.isConnectedToPort())
) {
model.model.x = diagramModel.getGridPosition(model.initialX + amountX / amountZoom);
model.model.y = diagramModel.getGridPosition(model.initialY + amountY / amountZoom);
} else if (model.model instanceof PointModel) {
// we want points that are connected to ports, to not neccesarilly snap to grid
// this stuff needs to be pixel perfect, dont touch it
model.model.x = model.initialX + diagramModel.getGridPosition(amountX / amountZoom);
model.model.y = model.initialY + diagramModel.getGridPosition(amountY / amountZoom);
}
});
this.fireAction();
if (!this.state.wasMoved) {
this.setState({ wasMoved: true });
} else {
this.forceUpdate();
}
} else if (this.state.action instanceof MoveCanvasAction) {
//translate the actual canvas
if (this.props.allowCanvasTranslation) {
diagramModel.setOffset(
this.state.action.initialOffsetX + (event.clientX - this.state.action.mouseX),
this.state.action.initialOffsetY + (event.clientY - this.state.action.mouseY)
);
this.fireAction();
this.forceUpdate();
}
}
}
onKeyUp(event) {
//delete all selected
if (this.props.deleteKeys.indexOf(event.keyCode) !== -1) {
_.forEach(this.props.diagramEngine.getDiagramModel().getSelectedItems(), element => {
//only delete items which are not locked
if (!this.props.diagramEngine.isModelLocked(element)) {
element.remove();
}
});
this.forceUpdate();
}
}
onMouseUp(event) {
var diagramEngine = this.props.diagramEngine;
//are we going to connect a link to something?
if (this.state.action instanceof MoveItemsAction) {
var element = this.getMouseElement(event);
var linkConnected = false;
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
if (element && element.model instanceof PortModel && !diagramEngine.isModelLocked(element.model)) {
linkConnected = true;
let link = model.model.getLink();
if (link.getTargetPort() !== null) {
//if this was a valid link already and we are adding a node in the middle, create 2 links from the original
if (link.getTargetPort() !== element.model && link.getSourcePort() !== element.model) {
const targetPort = link.getTargetPort();
let newLink = link.clone({});
newLink.setSourcePort(element.model);
newLink.setTargetPort(targetPort);
link.setTargetPort(element.model);
targetPort.removeLink(link);
newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]);
link.removePointsAfter(model.model);
diagramEngine.getDiagramModel().addLink(newLink);
//if we are connecting to the same target or source, remove tweener points
} else if (link.getTargetPort() === element.model) {
link.removePointsAfter(model.model);
} else if (link.getSourcePort() === element.model) {
link.removePointsBefore(model.model);
}
} else {
link.setTargetPort(element.model);
}
delete this.props.diagramEngine.linksThatHaveInitiallyRendered[link.getID()];
}
//if we moved a NodeModel and allowLooseLinks is false, we know that any links involved were valid
if ((!this.props.allowLooseLinks && element.model instanceof NodeModel) || !this.state.wasMoved) {
linkConnected = true;
}
});
//do we want to allow loose links on the diagram model or not
if (!linkConnected && !this.props.allowLooseLinks) {
_.forEach(this.state.action.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
var link = model.model.getLink();
if (link.isLastPoint(model.model)) {
link.remove();
}
});
}
diagramEngine.clearRepaintEntities();
this.stopFiringAction(!this.state.wasMoved);
} else {
diagramEngine.clearRepaintEntities();
this.stopFiringAction();
}
this.state.document.removeEventListener("mousemove", this.onMouseMove);
this.state.document.removeEventListener("mouseup", this.onMouseUp);
}
drawSelectionBox() {
let dimensions = (this.state.action as SelectingAction).getBoxDimensions();
return (
<div
className="selector"
style={{
top: dimensions.top,
left: dimensions.left,
width: dimensions.width,
height: dimensions.height
}}
/>
);
}
render() {
var diagramEngine = this.props.diagramEngine;
diagramEngine.setMaxNumberPointsPerLink(this.props.maxNumberPointsPerLink);
var diagramModel = diagramEngine.getDiagramModel();
return (
<div
ref={ref => {
if (ref) {
this.props.diagramEngine.setCanvas(ref);
}
}}
className="storm-diagrams-canvas"
onWheel={event => {
if (this.props.allowCanvasZoom) {
event.preventDefault();
event.stopPropagation();
const oldZoomFactor = diagramModel.getZoomLevel() / 100;
let scrollDelta = this.props.inverseZoom ? -event.deltaY : event.deltaY;
//check if it is pinch gesture
if (event.ctrlKey && scrollDelta % 1 !== 0) {
/*Chrome and Firefox sends wheel event with deltaY that
have fractional part, also `ctrlKey` prop of the event is true
though ctrl isn't pressed
*/
scrollDelta /= 3;
} else {
scrollDelta /= 60;
}
if (diagramModel.getZoomLevel() + scrollDelta > 10) {
diagramModel.setZoomLevel(diagramModel.getZoomLevel() + scrollDelta);
}
const zoomFactor = diagramModel.getZoomLevel() / 100;
const boundingRect = event.currentTarget.getBoundingClientRect();
const clientWidth = boundingRect.width;
const clientHeight = boundingRect.height;
// compute difference between rect before and after scroll
const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor;
const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor;
// compute mouse coords relative to canvas
const clientX = event.clientX - boundingRect.left;
const clientY = event.clientY - boundingRect.top;
// compute width and height increment factor
const xFactor = (clientX - diagramModel.getOffsetX()) / oldZoomFactor / clientWidth;
const yFactor = (clientY - diagramModel.getOffsetY()) / oldZoomFactor / clientHeight;
diagramModel.setOffset(
diagramModel.getOffsetX() - widthDiff * xFactor,
diagramModel.getOffsetY() - heightDiff * yFactor
);
diagramEngine.enableRepaintEntities([]);
this.forceUpdate();
}
}}
onMouseDown={event => {
this.setState({ ...this.state, wasMoved: false });
diagramEngine.clearRepaintEntities();
var model = this.getMouseElement(event);
//the canvas was selected
if (model === null) {
//is it a multiple selection
if (event.shiftKey) {
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
this.startFiringAction(new SelectingAction(relative.x, relative.y));
} else {
//its a drag the canvas event
diagramModel.clearSelection();
this.startFiringAction(new MoveCanvasAction(event.clientX, event.clientY, diagramModel));
}
} else if (model.model instanceof PortModel) {
//its a port element, we want to drag a link
if (!this.props.diagramEngine.isModelLocked(model.model)) {
var relative = diagramEngine.getRelativeMousePoint(event);
var sourcePort = model.model;
var link = sourcePort.createLinkModel();
link.getFirstPoint().updateLocation(relative);
link.getLastPoint().updateLocation(relative);
diagramModel.clearSelection();
link.getLastPoint().setSelected(true);
diagramModel.addLink(link);
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine));
} else {
diagramModel.clearSelection();
}
} else {
//its some or other element, probably want to move it
if (!event.shiftKey && !model.model.isSelected()) {
diagramModel.clearSelection();
}
model.model.setSelected(true);
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine));
}
this.state.document.addEventListener("mousemove", this.onMouseMove);
this.state.document.addEventListener("mouseup", this.onMouseUp);
}}
>
{this.state.renderedNodes && (
<LinkLayerWidget
diagramEngine={diagramEngine}
pointAdded={(point: PointModel, event) => {
this.state.document.addEventListener("mousemove", this.onMouseMove);
this.state.document.addEventListener("mouseup", this.onMouseUp);
event.stopPropagation();
diagramModel.clearSelection(point);
this.setState({
action: new MoveItemsAction(event.clientX, event.clientY, diagramEngine)
});
}}
/>
)}
<NodeLayerWidget diagramEngine={diagramEngine} />
{this.state.action instanceof SelectingAction && this.drawSelectionBox()}
</div>
);
}
}

View File

@@ -1,83 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { LinkWidget } from "./LinkWidget";
import * as _ from "lodash";
import { PointModel } from "../models/PointModel";
export interface LinkLayerProps {
diagramEngine: DiagramEngine;
pointAdded: (point: PointModel, event) => any;
}
export interface LinkLayerState {}
/**
* @author Dylan Vorster
*/
export class LinkLayerWidget extends React.Component<LinkLayerProps, LinkLayerState> {
constructor(props: LinkLayerProps) {
super(props);
this.state = {};
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<svg
style={{
transform:
"translate(" +
diagramModel.getOffsetX() +
"px," +
diagramModel.getOffsetY() +
"px) scale(" +
diagramModel.getZoomLevel() / 100.0 +
")",
width: "100%",
height: "100%"
}}
>
{//only perform these actions when we have a diagram
this.props.diagramEngine.canvas &&
_.map(diagramModel.getLinks(), link => {
if (
this.props.diagramEngine.nodesRendered &&
!this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id]
) {
if (link.sourcePort !== null) {
try {
link.points[0].updateLocation(
this.props.diagramEngine.getPortCenter(link.sourcePort)
);
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
} catch (ex) {}
}
if (link.targetPort !== null) {
try {
_.last(link.points).updateLocation(
this.props.diagramEngine.getPortCenter(link.targetPort)
);
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
} catch (ex) {}
}
}
//generate links
var generatedLink = this.props.diagramEngine.generateWidgetForLink(link);
if (!generatedLink) {
console.log("no link generated for type: " + link.getType());
return null;
}
return (
<LinkWidget key={link.getID()} link={link} diagramEngine={this.props.diagramEngine}>
{React.cloneElement(generatedLink, {
pointAdded: this.props.pointAdded
})}
</LinkWidget>
);
})}
</svg>
);
}
}

View File

@@ -1,29 +0,0 @@
import * as React from "react";
import { DiagramEngine } from "../DiagramEngine";
import { LinkModel } from "../models/LinkModel";
export interface LinkProps {
link: LinkModel;
diagramEngine: DiagramEngine;
children?: any;
}
export interface LinkState {}
/**
* @author Dylan Vorster
*/
export class LinkWidget extends React.Component<LinkProps, LinkState> {
constructor(props: LinkProps) {
super(props);
this.state = {};
}
shouldComponentUpdate() {
return this.props.diagramEngine.canEntityRepaint(this.props.link);
}
render() {
return this.props.children;
}
}

View File

@@ -1,58 +0,0 @@
import * as React from "react";
import { DiagramModel } from "../models/DiagramModel";
import { DiagramEngine } from "../DiagramEngine";
import * as _ from "lodash";
import { NodeWidget } from "./NodeWidget";
export interface NodeLayerProps {
diagramEngine: DiagramEngine;
}
export interface NodeLayerState {}
/**
* @author Dylan Vorster
*/
export class NodeLayerWidget extends React.Component<NodeLayerProps, NodeLayerState> {
constructor(props: NodeLayerProps) {
super(props);
this.state = {};
}
componentDidUpdate() {
this.props.diagramEngine.nodesRendered = true;
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<div
className="node-view"
style={{
transform:
"translate(" +
diagramModel.getOffsetX() +
"px," +
diagramModel.getOffsetY() +
"px) scale(" +
diagramModel.getZoomLevel() / 100.0 +
")",
width: "100%",
height: "100%"
}}
>
{_.map(diagramModel.getNodes(), node => {
return React.createElement(
NodeWidget,
{
diagramEngine: this.props.diagramEngine,
key: node.id,
node: node
},
this.props.diagramEngine.generateWidgetForNode(node)
);
})}
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More