Compare commits

..

78 Commits

Author SHA1 Message Date
Dylan Vorster
3823adc8e8 v6.0.0-alpha.4.2 2019-07-31 23:58:04 +02:00
Dylan Vorster
fce011a11b Merge pull request #379 from dudenamedjune/lerna
fixed path issue in lib-routing/src/link/PathFindingLinkWidget.tsx
2019-07-31 18:39:50 +02:00
dudenamedjune
4962343b54 fixed path issue in lib-routing/src/link/PathFindingLinkWidget.tsx 2019-07-31 11:22:29 -05:00
Dylan Vorster
45649146c9 v6.0.0-alpha.4.1 2019-07-30 22:53:42 +02:00
Dylan Vorster
74e24331ed fix flow demo 2019-07-30 22:48:57 +02:00
Dylan Vorster
0cc469fa44 storybook 2019-07-30 22:43:29 +02:00
Dylan Vorster
0b78ba69c6 fix up the builds 2019-07-30 22:36:44 +02:00
Dylan Vorster
ca301b9d4d v6.0.0-alpha.4.0 2019-07-30 22:23:18 +02:00
Dylan Vorster
ad6c77da28 v6.0.0-alpha.3.0 2019-07-30 22:15:19 +02:00
Dylan Vorster
66b72b4465 v6.0.0-y.0 2019-07-30 22:06:26 +02:00
Dylan Vorster
bb878657ba v6.0.0-alpha.1 2019-07-30 21:58:06 +02:00
Dylan Vorster
1db1e21fc2 hmm 2019-07-30 21:57:27 +02:00
Dylan Vorster
f4920ab4d0 v6.0.0-alpha.0 2019-07-30 20:18:37 +02:00
Dylan Vorster
ccfd8e026c some work 2019-07-30 20:18:21 +02:00
Dylan Vorster
702290d9b3 leaking logs 2019-07-30 01:54:47 +02:00
Dylan Vorster
347377d3b4 fixed a bunch of issues with links, and moved it all to styled components 2019-07-30 01:53:07 +02:00
Dylan Vorster
44fd2ad91b fix type issues 2019-07-30 00:12:30 +02:00
Dylan Vorster
39d49c9ee0 type issue fixes 2019-07-29 23:34:36 +02:00
Dylan Vorster
61c6d4b161 massive performance wins 2019-07-29 00:40:22 +02:00
Dylan Vorster
b324154eff pretty 2019-07-28 22:32:29 +02:00
Dylan Vorster
17350a740a fix points 2019-07-28 22:32:08 +02:00
Dylan Vorster
d7fc5370ab improved routing 2019-07-28 21:52:48 +02:00
Dylan Vorster
5fbe7ce9e4 pretty 2019-07-28 21:29:45 +02:00
Dylan Vorster
3e37e34155 generics 2019-07-28 20:23:15 +02:00
Dylan Vorster
f46680596c Fixed bugs after refactor 2019-07-28 18:28:18 +02:00
Dylan Vorster
3a2d3ccb73 make all the things pluggable 2019-07-28 18:13:03 +02:00
Dylan Vorster
f4aa1852ed improved actions 2019-07-28 16:08:37 +02:00
Dylan Vorster
e167885038 Start improving the action system 2019-07-28 14:56:47 +02:00
Dylan Vorster
619e8a2347 more parity 2019-07-27 23:07:04 +02:00
Dylan Vorster
4c1e67106a make the widgets report their dimensions 2019-07-27 22:53:04 +02:00
Dylan Vorster
d08be537e1 use geometry 2019-07-27 16:12:37 +02:00
Dylan Vorster
8e5243da95 more resize stuffs 2019-07-27 00:31:30 +02:00
Dylan Vorster
22236f8420 oops 2019-07-27 00:25:13 +02:00
Dylan Vorster
99fe94ae29 resize observers 2019-07-27 00:23:19 +02:00
Dylan Vorster
3b0c19d998 fixes 2019-07-26 23:15:24 +02:00
Dylan Vorster
fc67d477a9 start using better geometry 2019-07-26 22:38:21 +02:00
Dylan Vorster
e4d427e0d3 formatting 2019-07-26 17:33:45 +02:00
Dylan Vorster
bc2d444aee dagre stuffs 2019-07-26 17:33:27 +02:00
Dylan Vorster
b15a326dab Fixed labels 2019-07-26 16:17:46 +02:00
Dylan Vorster
b54feb03e4 fixed more stuff 2019-07-26 14:31:18 +02:00
Dylan Vorster
39eddc0a2a Fixed up the demos 2019-07-26 14:11:05 +02:00
Dylan Vorster
85a1fde2e5 fixes and improvements 2019-07-26 13:54:09 +02:00
Dylan Vorster
c0dae87d02 cleanup 2019-07-26 13:38:34 +02:00
Dylan Vorster
fddec93370 more generics 2019-07-26 13:37:30 +02:00
Dylan Vorster
01ffbce479 saftey push 2019-07-25 22:29:47 +02:00
Dylan Vorster
30790682d3 spacing 2019-07-25 15:58:34 +02:00
Dylan Vorster
dc9c2eae66 API improvements and start moving to emotion 2019-07-25 15:57:39 +02:00
Dylan Vorster
969152a814 links 2019-07-25 01:15:01 +02:00
Dylan Vorster
2e38a7cfd5 space 2019-07-25 01:13:04 +02:00
Dylan Vorster
e3fe7319c1 changelog 2019-07-25 01:12:19 +02:00
Dylan Vorster
c3a332ebab fine ill add it again 2019-07-25 01:00:59 +02:00
Dylan Vorster
314bb908f9 sigh 2019-07-25 00:59:04 +02:00
Dylan Vorster
9b16b3f4ed try this out for size 2019-07-25 00:56:13 +02:00
Dylan Vorster
722b6d98f3 tests work globally again 2019-07-25 00:54:48 +02:00
Dylan Vorster
2c6971be9a remove snapshot testing 2019-07-25 00:44:14 +02:00
Dylan Vorster
ea0160cbcf e2e tests 2019-07-25 00:32:56 +02:00
Dylan Vorster
efc53eec9b spelling 2019-07-24 16:40:43 +02:00
Dylan Vorster
b5c86159d1 some docs 2019-07-24 16:37:05 +02:00
Dylan Vorster
ea2070ec98 more cleanup 2019-07-24 16:32:05 +02:00
Dylan Vorster
39c3611686 formatting 2019-07-24 16:26:46 +02:00
Dylan Vorster
1ff63d4117 routing works again :yey: 2019-07-24 16:19:31 +02:00
Dylan Vorster
e80da8853f got the demos working again 2019-07-24 15:31:19 +02:00
Dylan Vorster
a06e23eaf6 more lerna work 2019-07-24 15:17:48 +02:00
Dylan Vorster
4b599fc37f getting started 2019-07-24 13:40:19 +02:00
Dylan Vorster
def92af022 lerna 2019-07-24 00:31:31 +02:00
Dylan Vorster
01e9f5f2db changes 2019-07-22 23:27:55 +02:00
Dylan Vorster
aa1a02774d 5.3.2 2019-07-22 23:26:57 +02:00
Dylan Vorster
e60aac56db 5.3.1 2019-07-22 23:26:54 +02:00
Dylan Vorster
b2e74d8b97 badge 2019-07-22 23:08:24 +02:00
Dylan Vorster
e333a4f168 correct version 2019-07-22 22:59:31 +02:00
Dylan Vorster
1d316409e1 npm 2019-07-22 22:57:10 +02:00
Dylan Vorster
9d947d40b7 5.3.1 2019-07-22 22:55:55 +02:00
Dylan Vorster
7d80f15358 improvements 2019-07-22 22:55:33 +02:00
Dylan Vorster
28844b86eb 5.3.0 2019-07-22 22:48:40 +02:00
Dylan Vorster
70c9aeac66 please 2 2019-07-22 22:44:15 +02:00
Dylan Vorster
12a7bfbda5 please 2019-07-22 21:58:46 +02:00
Dylan Vorster
650061db4a fix jest 2019-07-22 21:05:28 +02:00
Dylan Vorster
48c6d1f038 changelog 2019-07-22 20:42:53 +02:00
261 changed files with 9611 additions and 23328 deletions

View File

@@ -2,30 +2,21 @@ version: 2
jobs:
build:
docker:
- image: projectstorm/react-diagrams-ci
- image: buildkite/puppeteer
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
- v1-dependencies-{{ checksum "yarn.lock" }}
- run: yarn install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
key: v1-dependencies-{{ checksum "yarn.lock" }}
# test building project
- run: yarn run prepublishOnly
# test building storybook
- run: yarn run storybook:build
- run: yarn run build
# test e2e tests and jest snapshots
- run: yarn run test:ci
- run: yarn run test:ci

View File

@@ -4,21 +4,18 @@
- [ ] The tests pass on CircleCI
- [ ] You have referenced the issue(s) or other PR(s) this fixes/relates-to
- [ ] The PR Template has been filled out (see below)
- [ ] Had a beer because you are awesome
- [ ] Had a beer/coffee because you are awesome
## What?
(My awesome new feature does this really cool thing.)
## Why?
(Because obviously it could not do it before)
## How?
(Basically I did this and that because im a super 1337 hacker)
## Feel-Good "programming lol" image:
## Feel good image:
(Add your own one below :])

200
.gitignore vendored
View File

@@ -1,194 +1,8 @@
dist/
dist/main.js
dist/main.js.map
/package
*.tgz
@types/
.out
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node
#!! ERROR: net is undefined. Use list command to see defined gitignore types !!#
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
dist
.DS_Store
.idea
.out
*.zip
.env
node_modules
yarn-error.log

View File

@@ -1,194 +0,0 @@
demos
images
docs
.out
.storybook
.circleci
tests
*.md
# Created by https://www.gitignore.io/api/net,netbeans,sublimetext,phpstorm,windows,osx,node
#!! ERROR: net is undefined. Use list command to see defined gitignore types !!#
### NetBeans ###
nbproject/private/
build/
nbbuild/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
.idea

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
.out

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"jsxBracketSameLine": true,
"useTabs": true,
"printWidth": 120
}

View File

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

View File

@@ -1,64 +1,80 @@
__6.0.0__
* (maintenance) move to Lerna and break up the library
* (api)(breaking) smart routing is now a Link Factory
__5.3.2__
* (maintenance) Upgrade :allthethings: (all the build tooling was upgrade)
* (api) move to ES6 (JS now contains native classes)
* (api) changed package name to @projectstorm/react-diagrams
* (bug) (PR259)(https://github.com/projectstorm/react-diagrams/pull/259) Fixes #258
* (refactor) (PR 306)(https://github.com/projectstorm/react-diagrams/pull/306) `:any` fix
* (feature) (PR 178)(https://github.com/projectstorm/react-diagrams/pull/178) Trigger a positionChanged event when moving a Node that has the listener assigned.
* (fix) (PR 356)(https://github.com/projectstorm/react-diagrams/pull/356) Fixed Type issue with 'PointModel()'
* (demo) dark mode and upgrade storybook
__5.2.1__
* [fix] Always remove link from old source/target port on port change
* [maintenance] upgrade node modules
* [refactor] https://github.com/projectstorm/react-diagrams/commit/55f62587bd3b12513c7d37eff59edfc8bdb8d6c9
* [bug] https://github.com/projectstorm/react-diagrams/commit/75ef02dd4d131a0e7c08b2680c69efc390e50b84
* (fix) Always remove link from old source/target port on port change
* (maintenance) upgrade node modules
* (refactor) https://github.com/projectstorm/react-diagrams/commit/55f62587bd3b12513c7d37eff59edfc8bdb8d6c9
* (bug) https://github.com/projectstorm/react-diagrams/commit/75ef02dd4d131a0e7c08b2680c69efc390e50b84
-> and other improvements, also checkout the foundation work happening over at https://github.com/projectstorm/react-canvas
__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
* (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!
* (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
* (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
* (feature) zoom to fit
* added Circle CI tests
* [demo] dagre automatic layouts
* [demo] zoom to fit
* [demo] selection events
* [demo] limit number of points
* [demo] programmatic node updating
* (demo) dagre automatic layouts
* (demo) zoom to fit
* (demo) selection events
* (demo) limit number of points
* (demo) programmatic node updating
* updated dependencies
* [bugs] swapping diagram models in engines
* [bugs] issues with the rendering pipeline #107
* (bugs) swapping diagram models in engines
* (bugs) issues with the rendering pipeline #107
* added ci badge to Readme
__3.1.3__

View File

@@ -14,14 +14,15 @@ __PSA 2019__: I still want to jump onto the rewrite, but it is a much larger pro
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) [![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)
[![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/@projectstorm/react-diagrams.svg)](https://npmjs.org/package/@projectstorm/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) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/)
Example implementation using custom models:
![Personal Project](./images/example1.jpg)
![](./images/example2.jpg)
Example implementation using custom models: (Dylan's personal code)
![Personal Project](./docs/images/example1.jpg)
![](./docs/images/example2.jpg)
Get started with the default models right out of the box:
![](./images/example3.jpg)
![](./docs/images/example3.jpg)
## Introduction
@@ -31,16 +32,44 @@ A no-nonsense diagramming library written entirely in React with the help of a f
* 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
* Use HTML to create nodes, instead of SVG's
* 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
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages (Labview, Symlink etc..)
#### Run the demos
## Installing
For all the bells and whistles:
yarn add @projectstorm/react-diagrams
This includes all the packages listed below (and works like it used to before version 6.0)
### A more modular approach
This library now has a more modular design and you can import just the core (contains no default factories or routing)
yarn add @projectstorm/react-diagrams-core
and add some extras:
yarn add @projectstorm/react-diagrams-defaults
yarn add @projectstorm/react-diagrams-routing
## How to use
Take a look at the demos [lib-demo-gallery/demos](https://github.com/projectstorm/react-diagrams/tree/lerna/lib-demo-gallery/demos)
__or__
Take a look at the demo project: [lib-demo-project](https://github.com/projectstorm/react-diagrams/tree/lerna/lib-demo-project)
## Run the demos
After running `yarn install` you must then run: `yarn run storybook`
#### Building from source
## 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.
Simply run `yarn build` in the root directory \(or `NODE_ENV=production yarn build` 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\).
## [Checkout the docs](https://projectstorm.gitbooks.io/react-diagrams)

View File

@@ -1,23 +0,0 @@
import * as React from "react";
export class Helper {
/**
* Logs the mouse position in the console, but overlays a div that consumes all events
* since the actual story book stories are rendered as an iFrame.
*/
static logMousePosition() {
let element = window.parent.document.createElement("mouse-position");
element.style.position = "absolute";
element.style.top = "0px";
element.style.left = "0px";
element.style.bottom = "0px";
element.style.right = "0px";
element.style.zIndex = "10";
window.parent.document.body.appendChild(element);
window.parent.window.addEventListener("mousemove", event => {
console.clear();
console.log(event.clientX, event.clientY);
});
}
}

View File

@@ -1,77 +0,0 @@
@import "../../src/sass/main";
html, body, #root{
height: 100%;
padding: 0;
margin: 0;
}
.srd-demo-workspace{
background: black;
display: flex;
flex-direction: column;
height: 100%;
border-radius: 5px;
overflow: hidden;
&__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;
&:hover{
background: rgb(0,192,255);
}
}
}
&__content{
flex-grow: 1;
height: 100%;
}
}
.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;
.pointui{
fill: rgba(white,0.5);
}
}

View File

@@ -1,18 +0,0 @@
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.AbstractNodeFactory {
constructor() {
super("diamond");
}
generateReactWidget(diagramEngine: SRD.DiagramEngine, node: SRD.NodeModel): JSX.Element {
return <DiamonNodeWidget node={node} />;
}
getNewInstance() {
return new DiamondNodeModel();
}
}

View File

@@ -1,12 +0,0 @@
import { NodeModel } from "storm-react-diagrams";
import { DiamondPortModel } from "./DiamondPortModel";
export class DiamondNodeModel extends NodeModel {
constructor() {
super("diamond");
this.addPort(new DiamondPortModel("top"));
this.addPort(new DiamondPortModel("left"));
this.addPort(new DiamondPortModel("bottom"));
this.addPort(new DiamondPortModel("right"));
}
}

View File

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

View File

@@ -1,14 +0,0 @@
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,57 +0,0 @@
import {
DiagramEngine,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "storm-react-diagrams";
import * as React from "react";
// import the custom models
import { DiamondNodeModel } from "./DiamondNodeModel";
import { DiamondNodeFactory } from "./DiamondNodeFactory";
import { SimplePortFactory } from "./SimplePortFactory";
import { DiamondPortModel } from "./DiamondPortModel";
/**
* @Author Dylan Vorster
*/
export default () => {
//1) setup the diagram engine
var engine = new DiagramEngine();
engine.installDefaultFactories();
// register some other factories as well
engine.registerPortFactory(new SimplePortFactory("diamond", config => new DiamondPortModel()));
engine.registerNodeFactory(new DiamondNodeFactory());
//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, 150);
//3-B) create our new custom node
var node2 = new DiamondNodeModel();
node2.setPosition(250, 108);
var node3 = new DefaultNodeModel("Node 3", "red");
var port3 = node3.addInPort("In");
node3.setPosition(500, 150);
//3-C) link the 2 nodes together
var link1 = port1.link(node2.getPort("left"));
var link2 = port3.link(node2.getPort("right"));
//4) add the models to the root graph
model.addAll(node1, node2, node3, link1, link2);
//5) load model into engine
engine.setDiagramModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
};

View File

@@ -1,56 +0,0 @@
import * as dagre from "dagre";
import * as _ from "lodash";
const size = {
width: 60,
height: 60
};
export function distributeElements(model) {
let clonedModel = _.cloneDeep(model);
let nodes = distributeGraph(clonedModel);
nodes.forEach(node => {
let modelNode = clonedModel.nodes.find(item => item.id === node.id);
modelNode.x = node.x - node.width / 2;
modelNode.y = node.y - node.height / 2;
});
return clonedModel;
}
function distributeGraph(model) {
let nodes = mapElements(model);
let edges = mapEdges(model);
let graph = new dagre.graphlib.Graph();
graph.setGraph({});
graph.setDefaultEdgeLabel(() => ({}));
//add elements to dagre graph
nodes.forEach(node => {
graph.setNode(node.id, node.metadata);
});
edges.forEach(edge => {
if (edge.from && edge.to) {
graph.setEdge(edge.from, edge.to);
}
});
//auto-distribute
dagre.layout(graph);
return graph.nodes().map(node => graph.node(node));
}
function mapElements(model) {
// dagre compatible format
return model.nodes.map(node => ({ id: node.id, metadata: { ...size, id: node.id } }));
}
function mapEdges(model) {
// returns links which connects nodes
// we check are there both from and to nodes in the model. Sometimes links can be detached
return model.links
.map(link => ({
from: link.source,
to: link.target
}))
.filter(
item => model.nodes.find(node => node.id === item.from) && model.nodes.find(node => node.id === item.to)
);
}

View File

@@ -1,120 +0,0 @@
import {
DiagramEngine,
DefaultNodeFactory,
DefaultLinkFactory,
DiagramModel,
DefaultNodeModel,
LinkModel,
DefaultPortModel,
DiagramWidget
} from "storm-react-diagrams";
import { distributeElements } from "./dagre-utils";
import * as React from "react";
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
function createNode(name) {
return new DefaultNodeModel(name, "rgb(0,192,255)");
}
let count = 0;
function connectNodes(nodeFrom, nodeTo) {
//just to get id-like structure
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"));
return portOut.link(portTo);
}
/**
* Tests auto distribution
*/
class Demo8Widget extends React.Component<any, any> {
constructor(props) {
super(props);
this.state = {};
this.autoDistribute = this.autoDistribute.bind(this);
}
autoDistribute() {
const { engine } = this.props;
const model = engine.getDiagramModel();
let distributedModel = getDistributedModel(engine, model);
engine.setDiagramModel(distributedModel);
this.forceUpdate();
}
render() {
const { engine } = this.props;
return (
<DemoWorkspaceWidget buttons={<button onClick={this.autoDistribute}>Re-distribute</button>}>
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
</DemoWorkspaceWidget>
);
}
}
function getDistributedModel(engine, model) {
const serialized = model.serializeDiagram();
const distributedSerializedDiagram = distributeElements(serialized);
//deserialize the model
let deSerializedModel = new DiagramModel();
deSerializedModel.deSerializeDiagram(distributedSerializedDiagram, engine);
return deSerializedModel;
}
export default () => {
//1) setup the diagram engine
let engine = new DiagramEngine();
engine.installDefaultFactories();
//2) setup the diagram model
let model = new DiagramModel();
//3) create a default nodes
let nodesFrom = [];
let nodesTo = [];
nodesFrom.push(createNode("from-1"));
nodesFrom.push(createNode("from-2"));
nodesFrom.push(createNode("from-3"));
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);
return <Demo8Widget engine={engine} />;
};

View File

@@ -1,12 +0,0 @@
import * as React from "react";
import { BodyWidget } from "./components/BodyWidget";
import { Application } from "./Application";
import "./sass/main.scss";
export default () => {
var app = new Application();
return <BodyWidget app={app} />;
};

View File

@@ -1,47 +0,0 @@
.body{
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;
}
}
.content{
display: flex;
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-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,72 +0,0 @@
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.installDefaultFactories();
// 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 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.setDiagramModel(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,38 +0,0 @@
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.installDefaultFactories();
//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.setDiagramModel(model);
//6) render the diagram!
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} allowLooseLinks={false} />;
};

View File

@@ -1,9 +0,0 @@
# Simple Usage
Welcome to STORM React Diagrams (SRD). SRD is a no-nonsense easy to use library for creating
flow diagrams in the web that can ultimately represent any type of process/network/graph etc..
<!-- STORY -->
Try moving around one of the nodes or clicking and dragging the links to create new link anchors (points).
You can also zoom the canvas using the mouse wheel / scroll gesture and drag to select multiple entities on the graph by shift + dragging the mouse.

View File

@@ -1,68 +0,0 @@
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.installDefaultFactories();
// 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.setDiagramModel(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,71 +0,0 @@
import * as React from "react";
import {storiesOf, addDecorator, addParameters} from "@storybook/react";
import {setOptions} from "@storybook/addon-options";
import {host} from "storybook-host";
import {Helper} from "./.helpers/Helper";
import {Toolkit} from "../src/Toolkit";
import {themes} from '@storybook/theming';
addParameters({
options: {
theme: themes.dark,
},
});
//include the SCSS for the demo
import "./.helpers/demo.scss";
setOptions({
name: "STORM React Diagrams",
url: "https://github.com/projectstorm/react-diagrams",
addonPanelInRight: true
});
import demo_simple from "./demo-simple";
import demo_flow from "./demo-simple-flow";
import demo_performance from "./demo-performance";
import demo_locks from "./demo-locks";
import demo_grid from "./demo-grid";
import demo_limit_points from "./demo-limit-points";
import demo_listeners from "./demo-listeners";
import demo_zoom from "./demo-zoom-to-fit";
import demo_labels from "./demo-labelled-links";
storiesOf("Simple Usage", module)
.add("Simple example", demo_simple)
.add("Simple flow example", demo_flow)
.add("Performance demo", demo_performance)
.add("Locked widget", demo_locks)
.add("Canvas grid size", demo_grid)
.add("Limiting link points", demo_limit_points)
.add("Events and listeners", demo_listeners)
.add("Zoom to fit", demo_zoom)
.add("Links with labels", demo_labels);
import demo_adv_clone_selected from "./demo-cloning";
import demo_adv_ser_des from "./demo-serializing";
import demo_adv_prog from "./demo-mutate-graph";
import demo_adv_dnd from "./demo-drag-and-drop";
import demo_smart_routing from "./demo-smart-routing";
storiesOf("Advanced Techniques", module)
.add("Clone Selected", demo_adv_clone_selected)
.add("Serializing and de-serializing", demo_adv_ser_des)
.add("Programatically modifying graph", demo_adv_prog)
.add("Drag and drop", demo_adv_dnd)
.add("Smart routing", demo_smart_routing);
import demo_cust_nodes from "./demo-custom-node1";
import demo_cust_links from "./demo-custom-link1";
storiesOf("Custom Models", module)
.add("Custom diamond node", demo_cust_nodes)
.add("Custom animated links", demo_cust_links);
import demo_3rd_dagre from "./demo-dagre";
storiesOf("3rd party libraries", module)
.add("Auto Distribute (Dagre)", demo_3rd_dagre);
// enable this to log mouse location when writing new puppeteer tests
//Helper.logMousePosition()

View File

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

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 313 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 354 KiB

View File

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,27 +0,0 @@
const path = require("path");
// jest.config.js
module.exports = {
verbose: true,
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
transform: {
".*test_loader.*": path.join(__dirname, "tests", "helpers", "storybook-loader.js" ),
"^.+\\.tsx?$": "ts-jest",
},
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"
]
};

5
lerna.json Normal file
View File

@@ -0,0 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "6.0.0-alpha.4.2"
}

4
lib-all/.npmignore Normal file
View File

@@ -0,0 +1,4 @@
*
!dist/**/*
!package.json
!README.md

View File

37
lib-all/index.ts Normal file
View File

@@ -0,0 +1,37 @@
import {
DiagramEngine,
MoveCanvasActionFactory,
MoveItemsActionFactory,
SelectingItemsActionFactory
} from '@projectstorm/react-diagrams-core';
import {
DefaultLabelFactory,
DefaultLinkFactory,
DefaultNodeFactory,
DefaultPortFactory
} from '@projectstorm/react-diagrams-defaults';
import { PathFindingLinkFactory } from '@projectstorm/react-diagrams-routing';
export * from '@projectstorm/react-diagrams-core';
export * from '@projectstorm/react-diagrams-defaults';
export * from '@projectstorm/react-diagrams-routing';
/**
* Construct an engine with the defaults installed
*/
export default (): DiagramEngine => {
const engine = new DiagramEngine();
// register model factories
engine.getLabelFactories().registerFactory(new DefaultLabelFactory());
engine.getNodeFactories().registerFactory(new DefaultNodeFactory()); // i cant figure out why
engine.getLinkFactories().registerFactory(new DefaultLinkFactory());
engine.getLinkFactories().registerFactory(new PathFindingLinkFactory());
engine.getPortFactories().registerFactory(new DefaultPortFactory());
// register the default interaction behaviours
engine.getActionFactories().registerFactory(new MoveCanvasActionFactory());
engine.getActionFactories().registerFactory(new SelectingItemsActionFactory());
engine.getActionFactories().registerFactory(new MoveItemsActionFactory());
return engine;
};

36
lib-all/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@projectstorm/react-diagrams",
"version": "6.0.0-alpha.4.2",
"author": "dylanvorster",
"repository": {
"type": "git",
"url": "https://github.com/projectstorm/react-diagrams.git"
},
"scripts": {
"clean": "rm -rf ./dist",
"build": "../node_modules/.bin/webpack",
"build:prod": "NODE_ENV=production ../node_modules/.bin/webpack"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"web",
"diagram",
"diagrams",
"react",
"typescript",
"flowchart",
"simple",
"links",
"nodes"
],
"main": "./dist/index.js",
"typings": "./dist/@types/index",
"dependencies": {
"@projectstorm/react-diagrams-core": "^6.0.0-alpha.4.2",
"@projectstorm/react-diagrams-defaults": "^6.0.0-alpha.4.2",
"@projectstorm/react-diagrams-routing": "^6.0.0-alpha.4.2"
},
"gitHead": "bb878657ba0c2f81764f32901fd96158a0f8352e"
}

13
lib-all/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/@types"
},
"include": [
"./index.ts"
],
"exclude": [
"./dist"
]
}

View File

@@ -0,0 +1,8 @@
const config = require('../webpack.shared')(__dirname);
module.exports = {
...config,
output: {
...config.output,
library: 'projectstorm/react-diagrams'
}
};

4
lib-core/.npmignore Normal file
View File

@@ -0,0 +1,4 @@
*
!dist/**/*
!package.json
!README.md

15
lib-core/CHANGELOG.md Normal file
View File

@@ -0,0 +1,15 @@
# Changelog
## 6.0.0
### Breaking changes
* `AbstractFactory:getNewInstance` renamed to `AbstractFactory:generateModel` and now gets given an event object
so that we can add to the event object without relying on more parameters
* `AbstractFactory::generateReactWidget` now receives an event object
* Moved factories in the diagramEngine into `FactoryBank`'s, which means we can remove the listeners in the DiagramEngine.
methods such as factoryAdded and factoryRemoved are now available on the FactoryBank (better design that allows more control)
* `addListener` renamed to `registerListener`

3
lib-core/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Project STORM > React Diagrams > Core
This workspace houses the default models

43
lib-core/index.ts Normal file
View File

@@ -0,0 +1,43 @@
import './sass/main.scss';
export * from './src/core-actions/AbstractAction';
export * from './src/core-actions/AbstractActionFactory';
export * from './src/core-actions/AbstractMouseAction';
export * from './src/actions/move-canvas/MoveCanvasActionFactory';
export * from './src/actions/move-canvas/MoveCanvasAction';
export * from './src/actions/selecting-items/SelectingAction';
export * from './src/actions/selecting-items/SelectingItemsActionFactory';
export * from './src/actions/move-items/MoveItemsAction';
export * from './src/actions/move-items/MoveItemsActionFactory';
export * from './src/core/BaseObserver';
export * from './src/core/FactoryBank';
export * from './src/core/AbstractFactory';
export * from './src/core/AbstractModelFactory';
export * from './src/core/AbstractReactFactory';
export * from './src/core-models/BaseModel';
export * from './src/core-models/BasePositionModel';
export * from './src/core-models/BaseEntity';
export * from './src/models/DiagramModel';
export * from './src/models/LabelModel';
export * from './src/models/LinkModel';
export * from './src/models/PointModel';
export * from './src/models/PortModel';
export * from './src/models/SelectionModel';
export * from './src/models/NodeModel';
export * from './src/widgets/BaseWidget';
export * from './src/widgets/DiagramWidget';
export * from './src/widgets/layers/LinkLayerWidget';
export * from './src/widgets/layers/NodeLayerWidget';
export * from './src/widgets/LinkWidget';
export * from './src/widgets/NodeWidget';
export * from './src/widgets/PortWidget';
export * from './src/DiagramEngine';
export * from './src/Toolkit';

39
lib-core/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "@projectstorm/react-diagrams-core",
"version": "6.0.0-alpha.4.2",
"author": "dylanvorster",
"repository": {
"type": "git",
"url": "https://github.com/projectstorm/react-diagrams.git"
},
"scripts": {
"clean": "rm -rf ./dist",
"build": "../node_modules/.bin/webpack",
"build:prod": "NODE_ENV=production ../node_modules/.bin/webpack"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"web",
"diagram",
"diagrams",
"react",
"typescript",
"flowchart",
"simple",
"links",
"nodes"
],
"main": "./dist/index.js",
"typings": "./dist/@types/index",
"dependencies": {
"@projectstorm/react-diagrams-geometry": "^6.0.0-alpha.4.2"
},
"peerDependencies": {
"closest": "^0.0.1",
"lodash": "4.*",
"react": "16.*"
},
"gitHead": "bb878657ba0c2f81764f32901fd96158a0f8352e"
}

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);
}
}

View File

@@ -0,0 +1,11 @@
.srd-link-layer {
position: absolute;
height: 100%;
width: 100%;
transform-origin: 0 0;
overflow: visible !important;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

View File

@@ -0,0 +1,11 @@
.srd-node-layer {
top: 0;
left: 0;
right: 0;
bottom: 0;
position: absolute;
pointer-events: none;
transform-origin: 0 0;
width: 100%;
height: 100%;
}

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,10 @@
.srd-port {
width: 15px;
height: 15px;
background: rgba(white, 0.1);
&:hover,
&.selected {
background: rgb(192, 255, 0);
}
}

5
lib-core/sass/main.scss Normal file
View File

@@ -0,0 +1,5 @@
@import 'DiagramWidget';
@import 'LinkLayerWidget';
@import 'NodeLayerWidget';
@import 'NodeWidget';
@import 'PortWidget';

View File

@@ -0,0 +1,336 @@
import { BaseEntity } from './core-models/BaseEntity';
import { DiagramModel } from './models/DiagramModel';
import { BaseModel } from './core-models/BaseModel';
import { NodeModel } from './models/NodeModel';
import { PortModel } from './models/PortModel';
import { LinkModel } from './models/LinkModel';
import { LabelModel } from './models/LabelModel';
import { FactoryBank } from './core/FactoryBank';
import { AbstractReactFactory } from './core/AbstractReactFactory';
import { BaseListener, BaseObserver } from './core/BaseObserver';
import { Point } from '@projectstorm/react-diagrams-geometry';
import { Toolkit } from './Toolkit';
import { MouseEvent } from 'react';
import { AbstractActionFactory } from './core-actions/AbstractActionFactory';
import { AbstractModelFactory } from './core/AbstractModelFactory';
export interface DiagramEngineListener extends BaseListener {
canvasReady?(): void;
repaintCanvas?(): void;
rendered?(): void;
}
/**
* Passed as a parameter to the DiagramWidget
*/
export class DiagramEngine extends BaseObserver<DiagramEngineListener> {
protected nodeFactories: FactoryBank<AbstractReactFactory<NodeModel>>;
protected linkFactories: FactoryBank<AbstractReactFactory<LinkModel>>;
protected portFactories: FactoryBank<AbstractModelFactory<PortModel>>;
protected labelFactories: FactoryBank<AbstractReactFactory<LabelModel>>;
protected actionFactories: FactoryBank<AbstractActionFactory>;
diagramModel: DiagramModel;
canvas: HTMLDivElement;
maxNumberPointsPerLink: number;
constructor() {
super();
this.maxNumberPointsPerLink = 1000;
this.diagramModel = new DiagramModel();
// create banks for the different factory types
this.nodeFactories = new FactoryBank();
this.linkFactories = new FactoryBank();
this.portFactories = new FactoryBank();
this.labelFactories = new FactoryBank();
this.actionFactories = new FactoryBank();
const setup = (factory: FactoryBank) => {
factory.registerListener({
factoryAdded: event => {
event.factory.setDiagramEngine(this);
},
factoryRemoved: event => {
event.factory.setDiagramEngine(null);
}
});
};
setup(this.nodeFactories);
setup(this.linkFactories);
setup(this.portFactories);
setup(this.labelFactories);
setup(this.actionFactories);
this.canvas = null;
// this.linksThatHaveInitiallyRendered = {};
}
repaintCanvas() {
this.iterateListeners(listener => {
if (listener.repaintCanvas) {
listener.repaintCanvas();
}
});
}
/**
* Gets a model and element under the mouse cursor
*/
getMouseElement(event: MouseEvent): { model: BaseModel; element: Element } {
var target = event.target as Element;
var diagramModel = this.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;
}
/**
* Checks to see if a model is locked by running through
* its parents to see if they are locked first
*/
isModelLocked(model: BaseEntity) {
//always check the diagram model
if (this.diagramModel.isLocked()) {
return true;
}
return model.isLocked();
}
setCanvas(canvas?: HTMLDivElement) {
if (this.canvas !== canvas) {
this.canvas = canvas;
if (canvas) {
this.fireEvent({}, 'canvasReady');
}
}
}
setDiagramModel(model: DiagramModel) {
this.diagramModel = model;
}
getDiagramModel(): DiagramModel {
return this.diagramModel;
}
//!-------------- FACTORIES ------------
getNodeFactories() {
return this.nodeFactories;
}
getLinkFactories() {
return this.linkFactories;
}
getLabelFactories() {
return this.labelFactories;
}
getPortFactories() {
return this.portFactories;
}
getActionFactories() {
return this.actionFactories;
}
getFactoryForNode<F extends AbstractReactFactory<NodeModel>>(node: NodeModel | string) {
if (typeof node === 'string') {
return this.nodeFactories.getFactory(node);
}
return this.nodeFactories.getFactory(node.getType());
}
getFactoryForLink<F extends AbstractReactFactory<LinkModel>>(link: LinkModel | string) {
if (typeof link === 'string') {
return this.linkFactories.getFactory<F>(link);
}
return this.linkFactories.getFactory<F>(link.getType());
}
getFactoryForLabel<F extends AbstractReactFactory<LabelModel>>(label: LabelModel) {
if (typeof label === 'string') {
return this.labelFactories.getFactory(label);
}
return this.labelFactories.getFactory(label.getType());
}
getFactoryForPort<F extends AbstractModelFactory<PortModel>>(port: PortModel) {
if (typeof port === 'string') {
return this.portFactories.getFactory<F>(port);
}
return this.portFactories.getFactory<F>(port.getType());
}
generateWidgetForLink(link: LinkModel): JSX.Element {
return this.getFactoryForLink(link).generateReactWidget({ model: link });
}
generateWidgetForNode(node: NodeModel): JSX.Element {
return this.getFactoryForNode(node).generateReactWidget({ model: node });
}
getRelativeMousePoint(event): Point {
var point = this.getRelativePoint(event.clientX, event.clientY);
return new Point(
(point.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
(point.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
);
}
getRelativePoint(x, y): Point {
var canvasRect = this.canvas.getBoundingClientRect();
return new Point(x - canvasRect.left, y - canvasRect.top);
}
getNodeElement(node: NodeModel): Element {
const selector = this.canvas.querySelector(`.node[data-nodeid="${node.getID()}"]`);
if (selector === null) {
throw new Error('Cannot find Node element with nodeID: [' + node.getID() + ']');
}
return selector;
}
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): Point {
var sourceElement = this.getNodePortElement(port);
var sourceRect = sourceElement.getBoundingClientRect();
var rel = this.getRelativePoint(sourceRect.left, sourceRect.top);
return new Point(
sourceElement.offsetWidth / 2 +
(rel.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
sourceElement.offsetHeight / 2 +
(rel.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
);
}
/**
* Calculate rectangular coordinates of the port passed in.
*/
getPortCoords(
port: PortModel,
element?: HTMLDivElement
): {
x: number;
y: number;
width: number;
height: number;
} {
if (!this.canvas) {
throw new Error('Canvas needs to be set first');
}
if (!element) {
element = this.getNodePortElement(port);
}
const sourceRect = element.getBoundingClientRect();
const canvasRect = this.canvas.getBoundingClientRect() as ClientRect;
return {
x:
(sourceRect.left - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0) -
canvasRect.left,
y:
(sourceRect.top - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0) - canvasRect.top,
width: sourceRect.width,
height: sourceRect.height
};
}
/**
* Determine the width and height of the node passed in.
* It currently assumes nodes have a rectangular shape, can be overriden for customised shapes.
*/
getNodeDimensions(node: NodeModel): { width: number; height: number } {
if (!this.canvas) {
return {
width: 0,
height: 0
};
}
const nodeElement = this.getNodeElement(node);
const nodeRect = nodeElement.getBoundingClientRect();
return {
width: nodeRect.width,
height: nodeRect.height
};
}
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;
this.diagramModel.setZoomLevel(this.diagramModel.getZoomLevel() * zoomFactor);
this.diagramModel.setOffset(0, 0);
this.repaintCanvas();
}
}

40
lib-core/src/Toolkit.ts Normal file
View File

@@ -0,0 +1,40 @@
import * as closest from 'closest';
import { PointModel } from './models/PointModel';
export class Toolkit {
static TESTING: boolean = false;
static TESTING_UID = 0;
/**
* Generats a unique ID (thanks Stack overflow :3)
* @returns {String}
*/
public static UID(): string {
if (Toolkit.TESTING) {
Toolkit.TESTING_UID++;
return `${Toolkit.TESTING_UID}`;
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Finds the closest element as a polyfill
*
* @param {Element} element [description]
* @param {string} selector [description]
*/
public static closest(element: Element, selector: string) {
if (document.body.closest) {
return element.closest(selector);
}
return closest(element, selector);
}
public static generateLinePath(firstPoint: PointModel, lastPoint: PointModel): string {
return `M${firstPoint.getX()},${firstPoint.getY()} L ${lastPoint.getX()},${lastPoint.getY()}`;
}
}

View File

@@ -0,0 +1,28 @@
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
import { MouseEvent } from 'react';
import { DiagramEngine } from '../../DiagramEngine';
export class MoveCanvasAction extends AbstractMouseAction {
initialOffsetX: number;
initialOffsetY: number;
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
super(mouseX, mouseY, engine);
this.initialOffsetX = this.model.getOffsetX();
this.initialOffsetY = this.model.getOffsetY();
}
fireMouseMove(event: MouseEvent) {
//translate the actual canvas
this.model.setOffset(
this.initialOffsetX + (event.clientX - this.mouseX),
this.initialOffsetY + (event.clientY - this.mouseY)
);
}
fireMouseUp(event) {}
fireMouseDown(event) {
this.model.clearSelection();
}
}

View File

@@ -0,0 +1,17 @@
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
import { MoveCanvasAction } from './MoveCanvasAction';
import { MouseEvent } from 'react';
export class MoveCanvasActionFactory extends AbstractActionFactory<MoveCanvasAction> {
constructor() {
super('move-canvas');
}
generateAction(event: MouseEvent): MoveCanvasAction {
return new MoveCanvasAction(event.clientX, event.clientY, this.engine);
}
activate(event: ActionFactoryActivationEvent): boolean {
return !event.selectedEntity && !event.mouseEvent.shiftKey;
}
}

View File

@@ -0,0 +1,177 @@
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
import { SelectionModel } from '../../models/SelectionModel';
import { PointModel } from '../../models/PointModel';
import { NodeModel } from '../../models/NodeModel';
import { DiagramEngine } from '../../DiagramEngine';
import { BasePositionModel } from '../../core-models/BasePositionModel';
import * as _ from 'lodash';
import { PortModel } from '../../models/PortModel';
import { LinkModel } from '../../models/LinkModel';
import { MouseEvent } from 'react';
import { ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
export class MoveItemsAction extends AbstractMouseAction {
selectionModels: SelectionModel[];
moved: boolean;
allowLooseLinks: boolean;
constructor(mouseX: number, mouseY: number, diagramEngine: DiagramEngine, allowLooseLinks: boolean) {
super(mouseX, mouseY, diagramEngine);
this.allowLooseLinks = allowLooseLinks;
this.moved = false;
}
fireMouseMove(event: MouseEvent) {
let amountX = event.clientX - this.mouseX;
let amountY = event.clientY - this.mouseY;
let amountZoom = this.model.getZoomLevel() / 100;
_.forEach(this.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.setPosition(
this.model.getGridPosition(model.initialX + amountX / amountZoom),
this.model.getGridPosition(model.initialY + amountY / amountZoom)
);
} else if (model.model instanceof PointModel) {
// we want points that are connected to ports, to not necessarily snap to grid
// this stuff needs to be pixel perfect, dont touch it
model.model.setPosition(
model.initialX + this.model.getGridPosition(amountX / amountZoom),
model.initialY + this.model.getGridPosition(amountY / amountZoom)
);
}
});
this.moved = true;
}
fireMouseUp(event: MouseEvent) {
const element = this.engine.getMouseElement(event);
_.forEach(this.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
if (element && element.model instanceof PortModel && !this.engine.isModelLocked(element.model)) {
let link = model.model.getLink();
//if this was a valid link already and we are adding a node in the middle, create 2 links from the original
if (link.getTargetPort()) {
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);
link.getLastPoint().setPosition(this.engine.getPortCenter(element.model));
targetPort.removeLink(link);
newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]);
link.removePointsAfter(model.model);
this.engine.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);
}
}
// set the target port
else {
link.setTargetPort(element.model);
link.getLastPoint().setPosition(this.engine.getPortCenter(element.model));
}
}
});
//check for / remove any loose links in any models which have been moved
if (!this.allowLooseLinks && this.moved) {
_.forEach(this.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
let selectedPoint: PointModel = model.model;
let link: LinkModel = selectedPoint.getLink();
if (link.getSourcePort() === null || link.getTargetPort() === null) {
link.remove();
}
});
}
//remove any invalid links
_.forEach(this.selectionModels, model => {
//only care about points connecting to things
if (!(model.model instanceof PointModel)) {
return;
}
let link: LinkModel = model.model.getLink();
let sourcePort: PortModel = link.getSourcePort();
let targetPort: PortModel = link.getTargetPort();
if (sourcePort !== null && targetPort !== null) {
if (!sourcePort.canLinkToPort(targetPort)) {
//link not allowed
link.remove();
} else if (
_.some(
_.values(targetPort.getLinks()),
(l: LinkModel) => l !== link && (l.getSourcePort() === sourcePort || l.getTargetPort() === sourcePort)
)
) {
//link is a duplicate
link.remove();
}
}
});
}
fireMouseDown(event: ActionFactoryActivationEvent) {
// clear selection first?
if (!event.selectedModel.isSelected()) {
this.model.clearSelection();
}
if (event.selectedModel instanceof PortModel) {
//its a port element, we want to drag a link
if (!this.engine.isModelLocked(event.selectedModel)) {
const portCenter = this.engine.getPortCenter(event.selectedModel);
const sourcePort = event.selectedModel;
const link = sourcePort.createLinkModel();
link.setSourcePort(sourcePort);
if (link) {
link.removeMiddlePoints();
if (link.getSourcePort() !== sourcePort) {
link.setSourcePort(sourcePort);
}
link.setTargetPort(null);
link.getFirstPoint().setPosition(portCenter);
link.getLastPoint().setPosition(portCenter);
this.model.clearSelection();
link.getLastPoint().setSelected(true);
this.model.addLink(link);
}
}
} else {
event.selectedModel.setSelected(true);
}
const selectedItems = this.model.getSelectedItems().filter(item => {
if (!(item instanceof BasePositionModel)) {
return false;
}
return !this.engine.isModelLocked(item);
});
this.selectionModels = selectedItems.map((item: PointModel | NodeModel) => {
return {
model: item,
initialX: item.getX(),
initialY: item.getY()
};
});
}
}

View File

@@ -0,0 +1,32 @@
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
import { MouseEvent } from 'react';
import { MoveItemsAction } from './MoveItemsAction';
export interface MoveItemsActionFactoryOptions {
allowLooseLinks?: boolean;
}
export class MoveItemsActionFactory extends AbstractActionFactory<MoveItemsAction> {
options: MoveItemsActionFactoryOptions;
static NAME = 'move-items';
constructor(options: MoveItemsActionFactoryOptions = {}) {
super(MoveItemsActionFactory.NAME);
this.options = {
...options,
allowLooseLinks: options.allowLooseLinks == null ? true : options.allowLooseLinks
};
}
generateAction(event: MouseEvent): MoveItemsAction {
return new MoveItemsAction(event.clientX, event.clientY, this.engine, this.options.allowLooseLinks);
}
activate(event: ActionFactoryActivationEvent): boolean {
if (event.selectedModel) {
return !this.engine.isModelLocked(event.selectedModel);
}
return false;
}
}

View File

@@ -0,0 +1,72 @@
import { AbstractMouseAction } from '../../core-actions/AbstractMouseAction';
import { DiagramModel } from '../../models/DiagramModel';
import * as _ from 'lodash';
import { DiagramEngine } from '../../DiagramEngine';
import { MouseEvent } from 'react';
export class SelectingAction extends AbstractMouseAction {
mouseX2: number;
mouseY2: number;
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
super(mouseX, mouseY, engine);
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
);
}
fireMouseMove(event: MouseEvent) {
var relative = this.engine.getRelativePoint(event.clientX, event.clientY);
_.forEach(this.model.getNodes(), node => {
// TODO use geometry instead
if (this.containsElement(node.getX(), node.getY(), this.model)) {
node.setSelected(true);
}
});
_.forEach(this.model.getLinks(), link => {
var allSelected = true;
_.forEach(link.getPoints(), point => {
if (this.containsElement(point.getX(), point.getY(), this.model)) {
point.setSelected(true);
} else {
allSelected = false;
}
});
if (allSelected) {
link.setSelected(true);
}
});
this.mouseX2 = relative.x;
this.mouseY2 = relative.y;
}
fireMouseUp(event) {}
fireMouseDown(event) {}
}

View File

@@ -0,0 +1,17 @@
import { AbstractActionFactory, ActionFactoryActivationEvent } from '../../core-actions/AbstractActionFactory';
import { MouseEvent } from 'react';
import { SelectingAction } from './SelectingAction';
export class SelectingItemsActionFactory extends AbstractActionFactory<SelectingAction> {
constructor() {
super('select-items');
}
generateAction(event: MouseEvent): SelectingAction {
return new SelectingAction(event.clientX, event.clientY, this.engine);
}
activate(event: ActionFactoryActivationEvent): boolean {
return !event.selectedEntity && event.mouseEvent.shiftKey;
}
}

View File

@@ -0,0 +1,12 @@
import { DiagramEngine } from '../DiagramEngine';
import { DiagramModel } from '../models/DiagramModel';
export class AbstractAction {
engine: DiagramEngine;
model: DiagramModel;
constructor(engine: DiagramEngine) {
this.engine = engine;
this.model = engine.getDiagramModel();
}
}

View File

@@ -0,0 +1,16 @@
import { AbstractAction } from './AbstractAction';
import { AbstractFactory } from '../core/AbstractFactory';
import { MouseEvent } from 'react';
import { BaseModel } from '../core-models/BaseModel';
export interface ActionFactoryActivationEvent {
selectedModel: BaseModel;
selectedEntity: HTMLElement;
mouseEvent: MouseEvent;
}
export abstract class AbstractActionFactory<T extends AbstractAction = AbstractAction> extends AbstractFactory {
abstract activate(event: ActionFactoryActivationEvent): boolean;
abstract generateAction(event: MouseEvent): T;
}

View File

@@ -0,0 +1,21 @@
import { MouseEvent } from 'react';
import { AbstractAction } from './AbstractAction';
import { DiagramEngine } from '../DiagramEngine';
import { ActionFactoryActivationEvent } from './AbstractActionFactory';
export abstract class AbstractMouseAction extends AbstractAction {
protected mouseX: number;
protected mouseY: number;
constructor(mouseX: number, mouseY: number, engine: DiagramEngine) {
super(engine);
this.mouseX = mouseX;
this.mouseY = mouseY;
}
abstract fireMouseDown(event: ActionFactoryActivationEvent);
abstract fireMouseMove(event: MouseEvent);
abstract fireMouseUp(event: MouseEvent);
}

View File

@@ -0,0 +1,105 @@
import { Toolkit } from '../Toolkit';
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import { BaseEvent, BaseListener, BaseObserver } from '../core/BaseObserver';
export interface BaseEntityEvent<T extends BaseEntity = BaseEntity> extends BaseEvent {
entity: T;
}
export interface BaseEntityListener<T extends BaseEntity = BaseEntity> extends BaseListener {
lockChanged?(event: BaseEntityEvent<T> & { locked: boolean }): void;
}
export type BaseEntityType = 'node' | 'link' | 'port' | 'point';
export interface BaseEntityOptions {
id?: string;
locked?: boolean;
}
export type BaseEntityGenerics = {
LISTENER: BaseEntityListener;
OPTIONS: BaseEntityOptions;
};
export class BaseEntity<T extends BaseEntityGenerics = BaseEntityGenerics> extends BaseObserver<T['LISTENER']> {
protected options: T['OPTIONS'];
constructor(options: T['OPTIONS'] = {}) {
super();
this.options = {
id: Toolkit.UID(),
...options
};
}
getOptions() {
return this.options;
}
getID() {
return this.options.id;
}
doClone(lookupTable: { [s: string]: any } = {}, clone: any) {
/*noop*/
}
clone(lookupTable: { [s: string]: any } = {}) {
// try and use an existing clone first
if (lookupTable[this.options.id]) {
return lookupTable[this.options.id];
}
let clone = _.clone(this);
clone.options = {
...this.options,
id: Toolkit.UID()
};
clone.clearListeners();
lookupTable[this.options.id] = clone;
this.doClone(lookupTable, clone);
return clone;
}
clearListeners() {
this.listeners = {};
}
deSerialize(data: ReturnType<this['serialize']>, engine: DiagramEngine) {
this.options.id = data.id;
this.options.locked = data.locked;
}
serialize() {
return {
id: this.options.id,
locked: this.options.locked
};
}
fireEvent<L extends Partial<BaseEntityEvent> & object>(event: L, k: keyof T['LISTENER']) {
super.fireEvent(
{
entity: this,
...event
},
k
);
}
public isLocked(): boolean {
return this.options.locked;
}
public setLocked(locked: boolean = true) {
this.options.locked = locked;
this.fireEvent(
{
locked: locked
},
'lockChanged'
);
}
}

View File

@@ -0,0 +1,81 @@
import { BaseEntity, BaseEntityEvent, BaseEntityGenerics, BaseEntityListener, BaseEntityOptions } from './BaseEntity';
import { DiagramEngine } from '../DiagramEngine';
export interface BaseModelListener extends BaseEntityListener {
selectionChanged?(event: BaseEntityEvent<BaseModel> & { isSelected: boolean }): void;
entityRemoved?(event: BaseEntityEvent<BaseModel>): void;
}
export interface BaseModelOptions extends BaseEntityOptions {
type?: string;
selected?: boolean;
extras?: any;
}
export interface BaseModelGenerics extends BaseEntityGenerics {
LISTENER: BaseModelListener;
PARENT: BaseEntity;
OPTIONS: BaseModelOptions;
}
export class BaseModel<G extends BaseModelGenerics = BaseModelGenerics> extends BaseEntity<G> {
protected parent: G['PARENT'];
constructor(options: G['OPTIONS']) {
super(options);
}
getParent(): G['PARENT'] {
return this.parent;
}
setParent(parent: G['PARENT']) {
this.parent = parent;
}
getSelectedEntities(): Array<BaseModel> {
if (this.isSelected()) {
return [this];
}
return [];
}
serialize() {
return {
...super.serialize(),
type: this.options.type,
selected: this.options.selected,
extras: this.options.extras
};
}
deSerialize(data: ReturnType<this['serialize']>, engine: DiagramEngine) {
super.deSerialize(data, engine);
this.options.extras = data.extras;
this.options.selected = data.selected;
}
getType(): string {
return this.options.type;
}
isSelected(): boolean {
return this.options.selected;
}
setSelected(selected: boolean = true) {
this.options.selected = selected;
this.fireEvent(
{
isSelected: selected
},
'selectionChanged'
);
}
remove() {
this.fireEvent({}, 'entityRemoved');
}
}

View File

@@ -0,0 +1,62 @@
import { BaseModel, BaseModelGenerics, BaseModelListener, BaseModelOptions } from './BaseModel';
import { DiagramEngine } from '../DiagramEngine';
import { BaseEntityEvent } from './BaseEntity';
import { Point } from '@projectstorm/react-diagrams-geometry';
export interface BasePositionModelListener extends BaseModelListener {
positionChanged?(event: BaseEntityEvent<BasePositionModel>): void;
}
export interface BasePositionModelOptions extends BaseModelOptions {
position?: Point;
}
export interface BasePositionModelGenerics extends BaseModelGenerics {
LISTENER: BasePositionModelListener;
OPTIONS: BasePositionModelOptions;
}
export class BasePositionModel<G extends BasePositionModelGenerics = BasePositionModelGenerics> extends BaseModel<G> {
protected position: Point;
constructor(options: G['OPTIONS']) {
super(options);
this.position = options.position || new Point(0, 0);
}
setPosition(point: Point);
setPosition(x: number, y: number);
setPosition(x, y?) {
if (typeof x === 'object') {
this.position = x;
} else {
this.position = new Point(x, y);
}
this.fireEvent({}, 'positionChanged');
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.position = new Point(ob.x, ob.y);
}
serialize() {
return {
...super.serialize(),
x: this.position.x,
y: this.position.y
};
}
getPosition(): Point {
return this.position;
}
getX() {
return this.position.x;
}
getY() {
return this.position.y;
}
}

View File

@@ -0,0 +1,34 @@
import { DiagramEngine } from '../DiagramEngine';
import { FactoryBank } from './FactoryBank';
/**
* Base factory for all the different types of entities.
* Gets registered with the engine, and is used to generate models
*/
export abstract class AbstractFactory<T = any> {
/**
* Couples the factory with the models it generates
*/
protected type: string;
/**
* The engine gets injected when the factory is registered
*/
protected engine: DiagramEngine;
protected bank: FactoryBank;
constructor(type: string) {
this.type = type;
}
setDiagramEngine(engine: DiagramEngine) {
this.engine = engine;
}
setFactoryBank(bank: FactoryBank) {
this.bank = bank;
}
getType(): string {
return this.type;
}
}

View File

@@ -0,0 +1,13 @@
import { AbstractFactory } from './AbstractFactory';
import { BaseModel } from '../core-models/BaseModel';
export interface GenerateModelEvent {
initialConfig?: any;
}
export abstract class AbstractModelFactory<T extends BaseModel> extends AbstractFactory<T> {
/**
* Generates new models (the core factory pattern)
*/
abstract generateModel(event: GenerateModelEvent): T;
}

View File

@@ -0,0 +1,16 @@
import { BaseModel } from '../core-models/BaseModel';
import { AbstractModelFactory } from './AbstractModelFactory';
export interface GenerateWidgetEvent<T extends BaseModel> {
model: T;
}
/**
* Further extends the AbstractFactory to add widget generation capability.
*/
export abstract class AbstractReactFactory<T extends BaseModel = BaseModel> extends AbstractModelFactory<T> {
/**
* Generates React widgets from the model contained in the event object
*/
abstract generateReactWidget(event: GenerateWidgetEvent<T>): JSX.Element;
}

View File

@@ -0,0 +1,144 @@
import { Toolkit } from '../Toolkit';
import { FactoryBankListener } from './FactoryBank';
export interface BaseEvent {
firing: boolean;
stopPropagation: () => any;
}
export interface BaseEventProxy extends BaseEvent {
function: string;
}
/**
* Listeners are always in the form of an object that contains methods that take events
*/
export type BaseListener = {
/**
* Generic event that fires before a specific event was fired
*/
eventWillFire?: (event: BaseEvent & { function: string }) => void;
/**
* Generic event that fires after a specific event was fired (even if it was consumed)
*/
eventDidFire?: (event: BaseEvent & { function: string }) => void;
/**
* Type for other events that will fire
*/
[key: string]: (event: BaseEvent) => any;
};
export interface ListenerHandle {
/**
* Used to degister the listener
*/
deregister: () => any;
/**
* Original ID of the listener
*/
id: string;
/**
* Original Listener
*/
listner: BaseListener;
}
/**
* Base observer pattern class for working with listeners
*/
export class BaseObserver<L extends BaseListener = BaseListener> {
protected listeners: { [id: string]: L };
constructor() {
this.listeners = {};
}
private fireEventInternal(fire: boolean, k: keyof L, event: BaseEvent) {
this.iterateListeners(listener => {
// returning false here will instruct itteration to stop
if (!fire && !event.firing) {
return false;
}
// fire selected listener
if (listener[k]) {
listener[k](event as BaseEvent);
}
});
}
fireEvent<K extends keyof L>(event: Partial<Parameters<L[K]>[0]>, k: keyof L) {
event = {
firing: true,
stopPropagation: () => {
event.firing = false;
},
...event
};
// fire pre
this.fireEventInternal(true, 'eventWillFire', {
...event,
function: k
} as BaseEventProxy);
// fire main event
this.fireEventInternal(false, k, event as BaseEvent);
// fire post
this.fireEventInternal(true, 'eventDidFire', {
...event,
function: k
} as BaseEventProxy);
}
iterateListeners(cb: (listener: L) => any) {
for (let id in this.listeners) {
const res = cb(this.listeners[id]);
// cancel itteration on false
if (res === false) {
return;
}
}
}
getListenerHandle(listener: L): ListenerHandle {
for (let id in this.listeners) {
if (this.listeners[id] === listener) {
return {
id: id,
listner: listener,
deregister: () => {
delete this.listeners[id];
}
};
}
}
}
registerListener(listener: L): ListenerHandle {
const id = Toolkit.UID();
this.listeners[id] = listener;
return {
id: id,
listner: listener,
deregister: () => {
delete this.listeners[id];
}
};
}
deregisterListener(listener: L | ListenerHandle) {
if (typeof listener === 'object') {
(listener as ListenerHandle).deregister();
return true;
}
const handle = this.getListenerHandle(listener);
if (handle) {
handle.deregister();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,59 @@
import { BaseEvent, BaseListener, BaseObserver } from './BaseObserver';
import { AbstractFactory } from './AbstractFactory';
import * as _ from 'lodash';
export interface FactoryBankListener<F extends AbstractFactory = AbstractFactory> extends BaseListener {
/**
* Factory as added to rhe bank
*/
factoryAdded?: (event: BaseEvent & { factory: AbstractFactory }) => any;
/**
* Factory was removed from the bank
*/
factoryRemoved?: (event: BaseEvent & { factory: AbstractFactory }) => any;
}
type Param<T extends string> = Parameters<FactoryBankListener[T]>[0];
/**
* Store and managed Factories that extend from Abstractfactory
*/
export class FactoryBank<F extends AbstractFactory = AbstractFactory> extends BaseObserver<FactoryBankListener<F>> {
protected factories: { [type: string]: F };
constructor() {
super();
this.factories = {};
}
getFactories(): F[] {
return _.values(this.factories);
}
clearFactories() {
for (let factory in this.factories) {
this.deregisterFactory(factory);
}
}
getFactory<T extends F = F>(type: string): T {
if (!this.factories[type]) {
throw new Error(`Cannot find factory with type [${type}]`);
}
return this.factories[type] as T;
}
registerFactory(factory: F) {
factory.setFactoryBank(this);
this.factories[factory.getType()] = factory;
this.fireEvent<'factoryAdded'>({ factory }, 'factoryAdded');
}
deregisterFactory(type: string) {
const factory = this.factories[type];
factory.setFactoryBank(null);
delete this.factories[type];
this.fireEvent<'factoryRemoved'>({ factory }, 'factoryRemoved');
}
}

View File

@@ -0,0 +1,285 @@
import {
BaseEntity,
BaseEntityEvent,
BaseEntityGenerics,
BaseEntityListener,
BaseEntityOptions,
BaseEntityType
} from '../core-models/BaseEntity';
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import { LinkModel } from './LinkModel';
import { NodeModel } from './NodeModel';
import { PortModel } from './PortModel';
import { BaseModel } from '../core-models/BaseModel';
import { PointModel } from './PointModel';
export interface DiagramListener extends BaseEntityListener {
nodesUpdated?(event: BaseEntityEvent & { node: NodeModel; isCreated: boolean }): void;
linksUpdated?(event: BaseEntityEvent & { link: LinkModel; isCreated: boolean }): void;
offsetUpdated?(event: BaseEntityEvent<DiagramModel> & { offsetX: number; offsetY: number }): void;
zoomUpdated?(event: BaseEntityEvent<DiagramModel> & { zoom: number }): void;
gridUpdated?(event: BaseEntityEvent<DiagramModel> & { size: number }): void;
}
export interface DiagramModelOptions extends BaseEntityOptions {
offsetX?: number;
offsetY?: number;
zoom?: number;
gridSize?: number;
}
export interface DiagramModelGenerics extends BaseEntityGenerics {
LISTENER: DiagramListener;
OPTIONS: DiagramModelOptions;
}
export class DiagramModel<G extends DiagramModelGenerics = DiagramModelGenerics> extends BaseEntity<G> {
//models
protected links: { [s: string]: LinkModel };
protected nodes: { [s: string]: NodeModel };
rendered: boolean;
constructor(options: G['OPTIONS'] = {}) {
super({
zoom: 100,
gridSize: 0,
offsetX: 0,
offsetY: 0,
...options
});
this.links = {};
this.nodes = {};
this.rendered = false;
}
setGridSize(size: number = 0) {
this.options.gridSize = size;
this.fireEvent({ size: size }, 'gridUpdated');
}
getGridPosition(pos) {
if (this.options.gridSize === 0) {
return pos;
}
return this.options.gridSize * Math.floor((pos + this.options.gridSize / 2) / this.options.gridSize);
}
deSerializeDiagram(object: any, diagramEngine: DiagramEngine) {
this.deSerialize(object, diagramEngine);
this.options.offsetX = object.offsetX;
this.options.offsetY = object.offsetY;
this.options.zoom = object.zoom;
this.options.gridSize = object.gridSize;
// deserialize nodes
_.forEach(object.nodes, (node: any) => {
let nodeOb = diagramEngine.getFactoryForNode(node.type).generateModel({ initialConfig: node });
nodeOb.setParent(this);
nodeOb.deSerialize(node, diagramEngine);
this.addNode(nodeOb);
});
// deserialze links
_.forEach(object.links, (link: any) => {
let linkOb = diagramEngine.getFactoryForLink(link.type).generateModel({ initialConfig: link });
linkOb.setParent(this);
linkOb.deSerialize(link, diagramEngine);
this.addLink(linkOb);
});
}
serializeDiagram() {
return {
...this.serialize(),
offsetX: this.options.offsetX,
offsetY: this.options.offsetY,
zoom: this.options.zoom,
gridSize: this.options.gridSize,
links: _.map(this.links, link => {
return link.serialize();
}),
nodes: _.map(this.nodes, node => {
return node.serialize();
})
};
}
clearSelection(ignore: BaseModel | 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[] {
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.getPoints(), 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.options.zoom = zoom;
this.fireEvent({ zoom }, 'zoomUpdated');
}
setOffset(offsetX: number, offsetY: number) {
this.options.offsetX = offsetX;
this.options.offsetY = offsetY;
this.fireEvent({ offsetX, offsetY }, 'offsetUpdated');
}
setOffsetX(offsetX: number) {
this.setOffset(offsetX, this.options.offsetY);
}
setOffsetY(offsetY: number) {
this.setOffset(this.options.offsetX, offsetY);
}
getOffsetY() {
return this.options.offsetY;
}
getOffsetX() {
return this.options.offsetX;
}
getZoomLevel() {
return this.options.zoom;
}
getNode(node: string | NodeModel): NodeModel | null {
if (node instanceof NodeModel) {
return node;
}
if (!this.nodes[node]) {
return null;
}
return this.nodes[node];
}
getLink(link: string | LinkModel): LinkModel | null {
if (link instanceof LinkModel) {
return link;
}
if (!this.links[link]) {
return null;
}
return this.links[link];
}
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.registerListener({
entityRemoved: () => {
this.removeLink(link);
}
});
this.links[link.getID()] = link;
this.fireEvent(
{
link,
isCreated: true
},
'linksUpdated'
);
return link;
}
addNode(node: NodeModel): NodeModel {
node.registerListener({
entityRemoved: () => {
this.removeNode(node);
}
});
this.nodes[node.getID()] = node;
this.fireEvent({ node, isCreated: true }, 'nodesUpdated');
return node;
}
removeLink(link: LinkModel | string) {
link = this.getLink(link);
delete this.links[link.getID()];
this.fireEvent({ link, isCreated: false }, 'linksUpdated');
}
removeNode(node: NodeModel | string) {
node = this.getNode(node);
delete this.nodes[node.getID()];
this.fireEvent({ node, isCreated: false }, 'nodesUpdated');
}
getLinks(): { [s: string]: LinkModel } {
return this.links;
}
getNodes(): { [s: string]: NodeModel } {
return this.nodes;
}
}

View File

@@ -0,0 +1,37 @@
import { BaseModel, BaseModelGenerics, BaseModelOptions } from '../core-models/BaseModel';
import { DiagramEngine } from '../DiagramEngine';
import { LinkModel } from './LinkModel';
export interface LabelModelOptions extends BaseModelOptions {
offsetX?: number;
offsetY?: number;
}
export interface LabelModelGenerics extends BaseModelGenerics {
PARENT: LinkModel;
OPTIONS: LabelModelOptions;
}
export class LabelModel<G extends LabelModelGenerics = LabelModelGenerics> extends BaseModel<G> {
constructor(options: G['OPTIONS']) {
super({
...options,
offsetX: options.offsetX || 0,
offsetY: options.offsetY || 0
});
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.options.offsetX = ob.offsetX;
this.options.offsetY = ob.offsetY;
}
serialize() {
return {
...super.serialize(),
offsetX: this.options.offsetX,
offsetY: this.options.offsetY
};
}
}

View File

@@ -1,46 +1,63 @@
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 { DiagramEngine } from "../DiagramEngine";
import { DiagramModel } from "./DiagramModel";
import { BaseModel, BaseModelGenerics, BaseModelListener } from '../core-models/BaseModel';
import { PortModel } from './PortModel';
import { PointModel } from './PointModel';
import * as _ from 'lodash';
import { LabelModel } from './LabelModel';
import { DiagramEngine } from '../DiagramEngine';
import { BaseEntityEvent } from '../core-models/BaseEntity';
import { DiagramModel } from './DiagramModel';
import { Point } from '@projectstorm/react-diagrams-geometry';
export interface LinkModelListener extends BaseModelListener {
sourcePortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
sourcePortChanged?(event: BaseEntityEvent<LinkModel> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
targetPortChanged?(event: BaseEntityEvent<LinkModel> & { port: null | PortModel }): void;
}
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends BaseModel<DiagramModel, T> {
sourcePort: PortModel | null;
targetPort: PortModel | null;
labels: LabelModel[];
points: PointModel[];
extras: any;
export interface LinkModelGenerics extends BaseModelGenerics {
LISTENER: LinkModelListener;
PARENT: DiagramModel;
}
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 = {};
export class LinkModel<G extends LinkModelGenerics = LinkModelGenerics> extends BaseModel<G> {
protected sourcePort: PortModel | null;
protected targetPort: PortModel | null;
protected labels: LabelModel[];
protected points: PointModel[];
protected renderedPaths: SVGPathElement[];
constructor(options: G['OPTIONS']) {
super(options);
this.points = [
new PointModel({
link: this
}),
new PointModel({
link: this
})
];
this.sourcePort = null;
this.targetPort = null;
this.renderedPaths = [];
this.labels = [];
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.extras = ob.extras;
this.points = _.map(ob.points || [], (point: { x; y }) => {
var p = new PointModel(this, { x: point.x, y: point.y });
var p = new PointModel({
link: this,
position: new Point(point.x, point.y)
});
p.deSerialize(point, engine);
return p;
});
//deserialize labels
_.forEach(ob.labels || [], (label: any) => {
let labelOb = engine.getLabelFactory(label.type).getNewInstance();
let labelOb = engine.getFactoryForLabel(label.type).generateModel({});
labelOb.deSerialize(label, engine);
this.addLabel(labelOb);
});
@@ -62,20 +79,28 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
}
getRenderedPath(): SVGPathElement[] {
return this.renderedPaths;
}
setRenderedPaths(paths: SVGPathElement[]) {
this.renderedPaths = paths;
}
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,
return {
...super.serialize(),
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: _.map(this.points, point => {
return point.serialize();
}),
extras: this.extras,
labels: _.map(this.labels, label => {
return label.serialize();
})
});
};
}
doClone(lookupTable = {}, clone) {
@@ -113,7 +138,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
getPointModel(id: string): PointModel | null {
for (var i = 0; i < this.points.length; i++) {
if (this.points[i].id === id) {
if (this.points[i].getID() === id) {
return this.points[i];
}
}
@@ -156,11 +181,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
this.sourcePort.removeLink(this);
}
this.sourcePort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
if (listener.sourcePortChanged) {
listener.sourcePortChanged({ ...event, port: port });
}
});
this.fireEvent({ port }, 'sourcePortChanged');
}
getSourcePort(): PortModel {
@@ -179,11 +200,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
this.targetPort.removeLink(this);
}
this.targetPort = port;
this.iterateListeners((listener: LinkModelListener, event) => {
if (listener.targetPortChanged) {
listener.targetPortChanged({ ...event, port: port });
}
});
this.fireEvent({ port }, 'targetPortChanged');
}
point(x: number, y: number): PointModel {
@@ -199,6 +216,10 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
return this.points;
}
getLabels() {
return this.labels;
}
setPoints(points: PointModel[]) {
_.forEach(points, point => {
point.setParent(this);
@@ -231,6 +252,9 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
}
generatePoint(x: number = 0, y: number = 0): PointModel {
return new PointModel(this, { x: x, y: y });
return new PointModel({
link: this,
position: new Point(x, y)
});
}
}

View File

@@ -0,0 +1,146 @@
import { BaseModelListener } from '../core-models/BaseModel';
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import { BaseEntityEvent } from '../core-models/BaseEntity';
import { BasePositionModel, BasePositionModelGenerics } from '../core-models/BasePositionModel';
import { DiagramModel } from './DiagramModel';
import { PortModel } from './PortModel';
import { LinkModel } from './LinkModel';
import { Point } from '@projectstorm/react-diagrams-geometry';
export interface NodeModelListener extends BaseModelListener {
positionChanged?(event: BaseEntityEvent<NodeModel>): void;
}
export interface NodeModelGenerics extends BasePositionModelGenerics {
LISTENER: NodeModelListener;
PARENT: DiagramModel;
}
export class NodeModel<G extends NodeModelGenerics = NodeModelGenerics> extends BasePositionModel<G> {
protected ports: { [s: string]: PortModel };
// calculated post rendering so routing can be done correctly
width: number;
height: number;
constructor(options: G['OPTIONS']) {
super(options);
this.ports = {};
this.width = 0;
this.height = 0;
}
setPosition(point: Point);
setPosition(x: number, y: number);
setPosition(x, y?) {
let old = this.position;
super.setPosition(x, y);
// also update the port co-ordinates (for make glorious speed)
_.forEach(this.ports, port => {
_.forEach(port.getLinks(), link => {
let point = link.getPointForPort(port);
point.setPosition(point.getX() + x - old.x, point.getY() + y - old.y);
});
});
}
getSelectedEntities() {
let entities = super.getSelectedEntities();
// add the points of each link that are selected here
if (this.isSelected()) {
_.forEach(this.ports, port => {
entities = entities.concat(
_.map(port.getLinks(), link => {
return link.getPointForPort(port);
})
);
});
}
return entities;
}
deSerialize(ob: ReturnType<this['serialize']>, engine: DiagramEngine) {
super.deSerialize(ob, engine);
//deserialize ports
_.forEach(ob.ports, (port: any) => {
let portOb = engine.getFactoryForPort(port.type).generateModel({});
portOb.deSerialize(port, engine);
this.addPort(portOb);
});
}
serialize() {
return {
...super.serialize(),
ports: _.map(this.ports, port => {
return port.serialize();
})
};
}
doClone(lookupTable = {}, clone) {
// also clone the ports
clone.ports = {};
_.forEach(this.ports, port => {
clone.addPort(port.clone(lookupTable));
});
}
remove() {
super.remove();
_.forEach(this.ports, port => {
_.forEach(port.getLinks(), link => {
link.remove();
});
});
}
getPortFromID(id): PortModel | null {
for (var i in this.ports) {
if (this.ports[i].getID() === id) {
return this.ports[i];
}
}
return null;
}
getLink(id: string): LinkModel {
for (let portID in this.ports) {
const links = this.ports[portID].getLinks();
if (links[id]) {
return links[id];
}
}
}
getPort(name: string): PortModel | null {
return this.ports[name];
}
getPorts(): { [s: string]: PortModel } {
return this.ports;
}
removePort(port: PortModel) {
//clear the parent node reference
if (this.ports[port.getName()]) {
this.ports[port.getName()].setParent(null);
delete this.ports[port.getName()];
}
}
addPort(port: PortModel): PortModel {
port.setParent(this);
this.ports[port.getName()] = port;
return port;
}
updateDimensions({ width, height }: { width: number; height: number }) {
this.width = width;
this.height = height;
}
}

View File

@@ -0,0 +1,56 @@
import { BaseModelListener } from '../core-models/BaseModel';
import { LinkModel } from './LinkModel';
import {
BasePositionModel,
BasePositionModelGenerics,
BasePositionModelOptions
} from '../core-models/BasePositionModel';
export interface PointModelOptions extends Omit<BasePositionModelOptions, 'type'> {
link: LinkModel;
}
export interface PointModelGenerics {
PARENT: LinkModel;
OPTIONS: PointModelOptions;
LISTENER: BaseModelListener;
}
export class PointModel<G extends PointModelGenerics = PointModelGenerics> extends BasePositionModel<
G & BasePositionModelGenerics
> {
constructor(options: G['OPTIONS']) {
super({
...options,
type: 'point'
});
this.parent = options.link;
}
getSelectedEntities() {
if (super.isSelected() && !this.isConnectedToPort()) {
return [this];
}
return [];
}
isConnectedToPort(): boolean {
return this.parent.getPortForPoint(this) !== null;
}
getLink(): LinkModel {
return this.getParent();
}
remove() {
//clear references
if (this.parent) {
this.parent.removePoint(this);
}
super.remove();
}
isLocked() {
return super.isLocked() || this.getParent().isLocked();
}
}

View File

@@ -0,0 +1,143 @@
import { BaseModelOptions } from '../core-models/BaseModel';
import { NodeModel } from './NodeModel';
import { LinkModel } from './LinkModel';
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import {
BasePositionModel,
BasePositionModelGenerics,
BasePositionModelListener
} from '../core-models/BasePositionModel';
import { Point } from '@projectstorm/react-diagrams-geometry';
import { BaseEntityEvent } from '../core-models/BaseEntity';
export enum PortModelAlignment {
TOP = 'top',
LEFT = 'left',
BOTTOM = 'bottom',
RIGHT = 'right'
}
export interface PortModelListener extends BasePositionModelListener {
/**
* fires when it first receives positional information
*/
reportInitialPosition?: (event: BaseEntityEvent<PortModel>) => void;
}
export interface PortModelOptions extends BaseModelOptions {
alignment?: PortModelAlignment;
maximumLinks?: number;
name: string;
}
export interface PortModelGenerics extends BasePositionModelGenerics {
OPTIONS: PortModelOptions;
PARENT: NodeModel;
LISTENER: PortModelListener;
}
export class PortModel<G extends PortModelGenerics = PortModelGenerics> extends BasePositionModel<G> {
links: { [id: string]: LinkModel };
// calculated post rendering so routing can be done correctly
width: number;
height: number;
reportedPosition: boolean;
constructor(options: G['OPTIONS']) {
super(options);
this.links = {};
this.reportedPosition = false;
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.reportedPosition = false;
this.options.name = ob.name;
this.options.alignment = ob.alignment;
}
serialize() {
return {
...super.serialize(),
name: this.options.name,
alignment: this.options.alignment,
parentNode: this.parent.getID(),
links: _.map(this.links, link => {
return link.getID();
})
};
}
doClone(lookupTable = {}, clone) {
clone.links = {};
clone.parentNode = this.getParent().clone(lookupTable);
}
getNode(): NodeModel {
return this.getParent();
}
getName(): string {
return this.options.name;
}
getMaximumLinks(): number {
return this.options.maximumLinks;
}
setMaximumLinks(maximumLinks: number) {
this.options.maximumLinks = maximumLinks;
}
removeLink(link: LinkModel) {
delete this.links[link.getID()];
}
addLink(link: LinkModel) {
this.links[link.getID()] = link;
}
getLinks(): { [id: string]: LinkModel } {
return this.links;
}
public createLinkModel(): LinkModel | null {
if (_.isFinite(this.options.maximumLinks)) {
var numberOfLinks: number = _.size(this.links);
if (this.options.maximumLinks === 1 && numberOfLinks >= 1) {
return _.values(this.links)[0];
} else if (numberOfLinks >= this.options.maximumLinks) {
return null;
}
}
return null;
}
updateCoords(cords: { x: number; y: number; width: number; height: number }) {
const { x, y, width, height } = cords;
this.width = width;
this.height = height;
this.setPosition(x, y);
const center = new Point(x + width / 2, y + height / 2);
_.forEach(this.getLinks(), link => {
link.getPointForPort(this).setPosition(center.clone());
});
this.reportedPosition = true;
this.fireEvent(
{
entity: this
},
'reportInitialPosition'
);
}
canLinkToPort(port: PortModel): boolean {
return true;
}
isLocked() {
return super.isLocked() || this.getParent().isLocked();
}
}

View File

@@ -0,0 +1,7 @@
import { BaseModel } from '../core-models/BaseModel';
export interface SelectionModel {
model: BaseModel;
initialX: number;
initialY: number;
}

View File

@@ -0,0 +1 @@
export class ActionManager {}

View File

@@ -1,5 +1,4 @@
import * as React from "react";
import * as _ from "lodash";
import * as React from 'react';
export interface BaseWidgetProps {
/**
@@ -26,13 +25,11 @@ export class BaseWidget<P extends BaseWidgetProps = BaseWidgetProps, S = any> ex
}
bem(selector: string): string {
return (this.props.baseClass || this.className) + selector + " ";
return (this.props.baseClass || this.className) + selector + ' ';
}
getClassName(): string {
return (
(this.props.baseClass || this.className) + " " + (this.props.className ? this.props.className + " " : "")
);
return (this.props.baseClass || this.className) + ' ' + (this.props.className ? this.props.className + ' ' : '');
}
getProps(): any {

View File

@@ -0,0 +1,288 @@
import * as React from 'react';
import { DiagramEngine } from '../DiagramEngine';
import * as _ from 'lodash';
import { LinkLayerWidget } from './layers/LinkLayerWidget';
import { NodeLayerWidget } from './layers/NodeLayerWidget';
import { AbstractMouseAction } from '../core-actions/AbstractMouseAction';
import { MoveItemsAction } from '../actions/move-items/MoveItemsAction';
import { SelectingAction } from '../actions/selecting-items/SelectingAction';
import { PointModel } from '../models/PointModel';
import { BaseWidget, BaseWidgetProps } from './BaseWidget';
import { MouseEvent } from 'react';
import { ActionFactoryActivationEvent } from '../core-actions/AbstractActionFactory';
import { AbstractAction } from '../core-actions/AbstractAction';
import { MoveItemsActionFactory } from '../actions/move-items/MoveItemsActionFactory';
export interface DiagramProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
// zoom
allowCanvasZoom?: boolean;
inverseZoom?: boolean;
actionStartedFiring?: (action: AbstractAction) => boolean;
actionStillFiring?: (action: AbstractAction) => void;
actionStoppedFiring?: (action: AbstractAction) => void;
deleteKeys?: number[];
}
export interface DiagramState {
action: AbstractAction;
diagramEngineListener: any;
}
export class DiagramWidget extends BaseWidget<DiagramProps, DiagramState> {
onKeyUpPointer: (this: Window, ev: KeyboardEvent) => void = null;
ref: React.RefObject<HTMLDivElement>;
constructor(props: DiagramProps) {
super('srd-diagram', props);
this.ref = React.createRef();
this.state = {
action: null,
diagramEngineListener: null
};
}
componentWillUnmount() {
this.props.diagramEngine.deregisterListener(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.deregisterListener(this.state.diagramEngineListener);
const diagramEngineListener = nextProps.diagramEngine.registerListener({
repaintCanvas: () => this.forceUpdate()
});
this.setState({ diagramEngineListener });
}
}
registerCanvas() {
this.props.diagramEngine.setCanvas(this.ref.current);
this.props.diagramEngine.iterateListeners(list => {
list.rendered && list.rendered();
});
}
componentDidUpdate() {
this.registerCanvas();
}
componentDidMount() {
this.onKeyUpPointer = this.onKeyUp.bind(this);
//add a keyboard listener
this.setState({
diagramEngineListener: this.props.diagramEngine.registerListener({
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();
}
this.registerCanvas();
}
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: AbstractAction) {
var setState = true;
if (this.props.actionStartedFiring) {
setState = this.props.actionStartedFiring(action);
}
if (setState) {
this.setState({ action: action });
}
}
onMouseUp = event => {
if (this.state.action && this.state.action instanceof AbstractMouseAction) {
this.state.action.fireMouseUp(event);
}
this.stopFiringAction();
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
};
onMouseMove = event => {
//select items so draw a bounding box
if (this.state.action) {
if (this.state.action && this.state.action instanceof AbstractMouseAction) {
this.state.action.fireMouseMove(event);
}
this.fireAction();
this.forceUpdate();
}
};
onKeyUp = event => {
//delete all selected
if ((this.props.deleteKeys || [46, 8]).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();
}
};
drawSelectionBox() {
let dimensions = (this.state.action as SelectingAction).getBoxDimensions();
return (
<div
className={this.bem('__selector')}
style={{
top: dimensions.top,
left: dimensions.left,
width: dimensions.width,
height: dimensions.height
}}
/>
);
}
getActionForEvent(event: MouseEvent): AbstractAction {
event.persist();
const { diagramEngine } = this.props;
const model = diagramEngine.getMouseElement(event);
const activateEvent: ActionFactoryActivationEvent = {
selectedModel: model && model.model,
selectedEntity: model && (model.element as HTMLElement),
mouseEvent: event
};
for (let factory of diagramEngine.getActionFactories().getFactories()) {
if (factory.activate(activateEvent)) {
return factory.generateAction(event);
}
}
return null;
}
render() {
const diagramEngine = this.props.diagramEngine;
const diagramModel = diagramEngine.getDiagramModel();
return (
<div
{...this.getProps()}
ref={this.ref}
onWheel={event => {
const allow = this.props.allowCanvasZoom == null ? true : this.props.allowCanvasZoom;
if (allow) {
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
);
this.forceUpdate();
}
}}
onMouseDown={event => {
// try and get an action for this event
const action = this.getActionForEvent(event);
if (action) {
if (action instanceof AbstractMouseAction) {
const selected = diagramEngine.getMouseElement(event);
action.fireMouseDown({
mouseEvent: event,
selectedEntity: selected && (selected.element as HTMLElement),
selectedModel: selected && selected.model
});
}
this.startFiringAction(action);
}
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
}}>
<LinkLayerWidget
diagramEngine={diagramEngine}
pointAdded={(point: PointModel, event) => {
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
event.stopPropagation();
// TODO implement this better and more generic
let action: MoveItemsAction = null;
let fac: MoveItemsActionFactory = null;
try {
fac = diagramEngine.getActionFactories().getFactory<MoveItemsActionFactory>(MoveItemsActionFactory.NAME);
} catch (e) {}
if (fac) {
action = fac.generateAction(event);
action.fireMouseDown({
selectedModel: point,
selectedEntity: event.target as HTMLElement,
mouseEvent: event
});
this.startFiringAction(action);
}
}}
/>
<NodeLayerWidget diagramEngine={diagramEngine} />
{this.state.action instanceof SelectingAction && this.drawSelectionBox()}
</div>
);
}
}

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { DiagramEngine } from '../DiagramEngine';
import { LabelModel } from '../models/LabelModel';
import styled from '@emotion/styled';
export interface LabelWidgetProps {
engine: DiagramEngine;
label: LabelModel;
index: number;
}
namespace S {
export const Label = styled.div`
display: inline-block;
position: absolute;
`;
export const Foreign = styled.foreignObject`
pointer-events: none;
`;
}
export class LabelWidget extends React.Component<LabelWidgetProps> {
ref: React.RefObject<HTMLDivElement>;
constructor(props: LabelWidgetProps) {
super(props);
this.ref = React.createRef();
}
componentDidUpdate() {
window.requestAnimationFrame(this.calculateLabelPosition);
}
componentDidMount() {
window.requestAnimationFrame(this.calculateLabelPosition);
}
findPathAndRelativePositionToRenderLabel = (index: number): { path: SVGPathElement; position: number } => {
// an array to hold all path lengths, making sure we hit the DOM only once to fetch this information
const link = this.props.label.getParent();
const lengths = link.getRenderedPath().map(path => path.getTotalLength());
// calculate the point where we want to display the label
let labelPosition =
lengths.reduce((previousValue, currentValue) => previousValue + currentValue, 0) *
(index / (link.getLabels().length + 1));
// find the path where the label will be rendered and calculate the relative position
let pathIndex = 0;
while (pathIndex < link.getRenderedPath().length) {
if (labelPosition - lengths[pathIndex] < 0) {
return {
path: link.getRenderedPath()[pathIndex],
position: labelPosition
};
}
// keep searching
labelPosition -= lengths[pathIndex];
pathIndex++;
}
};
calculateLabelPosition = () => {
const found = this.findPathAndRelativePositionToRenderLabel(this.props.index + 1);
if (!found) {
return;
}
const { path, position } = found;
const labelDimensions = {
width: this.ref.current.offsetWidth,
height: this.ref.current.offsetHeight
};
const pathCentre = path.getPointAtLength(position);
const labelCoordinates = {
x: pathCentre.x - labelDimensions.width / 2 + this.props.label.getOptions().offsetX,
y: pathCentre.y - labelDimensions.height / 2 + this.props.label.getOptions().offsetY
};
this.ref.current.style.transform = `translate(${labelCoordinates.x}px, ${labelCoordinates.y}px)`;
};
render() {
const canvas = this.props.engine.canvas;
return (
<S.Foreign key={this.props.label.getID()} width={canvas.offsetWidth} height={canvas.offsetHeight}>
<S.Label ref={this.ref}>
{this.props.engine.getFactoryForLabel(this.props.label).generateReactWidget({ model: this.props.label })}
</S.Label>
</S.Foreign>
);
}
}

View File

@@ -0,0 +1,118 @@
import * as React from 'react';
import { DiagramEngine } from '../DiagramEngine';
import { LinkModel } from '../models/LinkModel';
import { ListenerHandle } from '../core/BaseObserver';
import { BaseEntityEvent } from '../core-models/BaseEntity';
import { BasePositionModel } from '../core-models/BasePositionModel';
import { PointModel } from '../models/PointModel';
import { PortModel } from '../models/PortModel';
import { MouseEvent } from 'react';
import * as _ from 'lodash';
import { LabelWidget } from './LabelWidget';
import { PeformanceWidget } from './PeformanceWidget';
export interface LinkProps {
link: LinkModel;
diagramEngine: DiagramEngine;
pointAdded: (point: PointModel, event: MouseEvent) => any;
}
export interface LinkState {
sourceID: PortModel;
targetID: PortModel;
}
export class LinkWidget extends React.Component<LinkProps, LinkState> {
sourceListener: ListenerHandle;
targetListener: ListenerHandle;
constructor(props) {
super(props);
this.state = {
sourceID: null,
targetID: null
};
}
componentWillUnmount(): void {
if (this.sourceListener) {
this.sourceListener.deregister();
}
if (this.targetListener) {
this.targetListener.deregister();
}
}
static getDerivedStateFromProps(nextProps: LinkProps, prevState: LinkState): LinkState {
return {
sourceID: nextProps.link.getSourcePort(),
targetID: nextProps.link.getTargetPort()
};
}
ensureInstalled(installSource: boolean, installTarget: boolean) {
if (installSource) {
this.sourceListener = this.props.link.getSourcePort().registerListener({
reportInitialPosition: (event: BaseEntityEvent<BasePositionModel>) => {
this.forceUpdate();
}
});
}
if (installTarget) {
this.targetListener = this.props.link.getTargetPort().registerListener({
reportInitialPosition: (event: BaseEntityEvent<BasePositionModel>) => {
this.forceUpdate();
}
});
}
}
componentDidUpdate(prevProps: Readonly<LinkProps>, prevState: Readonly<LinkState>, snapshot?: any): void {
let installSource = false;
let installTarget = false;
if (this.state.sourceID !== prevState.sourceID) {
this.sourceListener && this.sourceListener.deregister();
installSource = true;
}
if (this.state.targetID !== prevState.targetID) {
this.targetListener && this.targetListener.deregister();
installTarget = true;
}
this.ensureInstalled(installSource, installTarget);
}
componentDidMount(): void {
this.ensureInstalled(!!this.props.link.getSourcePort(), !!this.props.link.getTargetPort());
}
render() {
const { link } = this.props;
// only draw the link when we have reported positions
if (link.getSourcePort() && !link.getSourcePort().reportedPosition) {
return null;
}
if (link.getTargetPort() && !link.getTargetPort().reportedPosition) {
return null;
}
//generate links
return (
<PeformanceWidget serialized={this.props.link.serialize()}>
{() => {
return (
<g>
{React.cloneElement(this.props.diagramEngine.generateWidgetForLink(link), {
pointAdded: this.props.pointAdded
})}
{_.map(this.props.link.getLabels(), (labelModel, index) => {
return <LabelWidget engine={this.props.diagramEngine} label={labelModel} index={index} />;
})}
</g>
);
}}
</PeformanceWidget>
);
}
}

View File

@@ -0,0 +1,86 @@
import * as React from 'react';
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import { NodeModel } from '../models/NodeModel';
import { BaseWidget, BaseWidgetProps } from './BaseWidget';
import { BaseEntityEvent } from '../core-models/BaseEntity';
import { BaseModel } from '../core-models/BaseModel';
import { ListenerHandle } from '../core/BaseObserver';
import { PeformanceWidget } from './PeformanceWidget';
export interface NodeProps extends BaseWidgetProps {
node: NodeModel;
children?: any;
diagramEngine: DiagramEngine;
}
export class NodeWidget extends BaseWidget<NodeProps> {
ob: any;
ref: React.RefObject<HTMLDivElement>;
listener: ListenerHandle;
constructor(props: NodeProps) {
super('srd-node', props);
this.ref = React.createRef();
}
getClassName() {
return 'node ' + super.getClassName() + (this.props.node.isSelected() ? this.bem('--selected') : '');
}
componentWillUnmount(): void {
this.ob.disconnect();
this.ob = null;
}
componentDidUpdate(prevProps: Readonly<NodeProps>, prevState: Readonly<any>, snapshot?: any): void {
if (this.listener && this.props.node !== prevProps.node) {
this.listener.deregister();
this.installSelectionListener();
}
}
installSelectionListener() {
this.listener = this.props.node.registerListener({
selectionChanged: (event: BaseEntityEvent<BaseModel> & { isSelected: boolean }) => {
this.forceUpdate();
}
});
}
componentDidMount(): void {
// @ts-ignore
this.ob = new ResizeObserver(entities => {
const bounds = entities[0].contentRect;
this.props.node.updateDimensions({ width: bounds.width, height: bounds.height });
//now mark the links as dirty
_.forEach(this.props.node.getPorts(), port => {
port.updateCoords(this.props.diagramEngine.getPortCoords(port));
});
});
this.ob.observe(this.ref.current);
this.installSelectionListener();
}
render() {
return (
<PeformanceWidget serialized={this.props.node.serialize()}>
{() => {
return (
<div
ref={this.ref}
{...this.getProps()}
data-nodeid={this.props.node.getID()}
style={{
top: this.props.node.getY(),
left: this.props.node.getX()
}}>
{this.props.diagramEngine.generateWidgetForNode(this.props.node)}
</div>
);
}}
</PeformanceWidget>
);
}
}

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import * as _ from 'lodash';
export interface PeformanceWidgetProps {
children: () => JSX.Element;
serialized: object;
}
export interface PeformanceWidgetState {}
export class PeformanceWidget extends React.Component<PeformanceWidgetProps, PeformanceWidgetState> {
shouldComponentUpdate(
nextProps: Readonly<PeformanceWidgetProps>,
nextState: Readonly<PeformanceWidgetState>,
nextContext: any
): boolean {
return !_.isEqual(this.props.serialized, nextProps.serialized);
}
render() {
return this.props.children();
}
}

View File

@@ -0,0 +1,86 @@
import * as React from 'react';
import * as _ from 'lodash';
import { BaseWidget, BaseWidgetProps } from './BaseWidget';
import { Toolkit } from '../Toolkit';
import { PortModel } from '../models/PortModel';
import { DiagramEngine } from '../DiagramEngine';
import { ListenerHandle } from '../core/BaseObserver';
export interface PortProps extends BaseWidgetProps {
port: PortModel;
engine: DiagramEngine;
}
export interface PortState {
selected: boolean;
}
export class PortWidget extends BaseWidget<PortProps, PortState> {
ref: React.RefObject<HTMLDivElement>;
engineListenerHandle: ListenerHandle;
constructor(props: PortProps) {
super('srd-port', props);
this.ref = React.createRef();
this.state = {
selected: false
};
}
report() {
this.props.port.updateCoords(this.props.engine.getPortCoords(this.props.port, this.ref.current));
}
componentWillUnmount(): void {
this.engineListenerHandle.deregister();
}
componentDidUpdate(prevProps: Readonly<PortProps>, prevState: Readonly<PortState>, snapshot?: any): void {
if (!this.props.port.reportedPosition) {
this.report();
}
}
componentDidMount(): void {
this.engineListenerHandle = this.props.engine.registerListener({
canvasReady: () => {
this.report();
}
});
if (this.props.engine.canvas) {
this.report();
}
}
getClassName() {
return 'port ' + super.getClassName() + (this.state.selected ? this.bem('--selected') : '');
}
getExtraProps() {
if (Toolkit.TESTING) {
const links = _.keys(this.props.port.getNode().getPort(this.props.port.getName()).links).join(',');
return {
'data-links': links
};
}
return {};
}
render() {
return (
<div
ref={this.ref}
{...this.getProps()}
onMouseEnter={() => {
this.setState({ selected: true });
}}
onMouseLeave={() => {
this.setState({ selected: false });
}}
data-name={this.props.port.getName()}
data-nodeid={this.props.port.getNode().getID()}
{...this.getExtraProps()}
/>
);
}
}

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import { DiagramEngine } from '../../DiagramEngine';
import { LinkWidget } from '../LinkWidget';
import * as _ from 'lodash';
import { PointModel } from '../../models/PointModel';
import { BaseWidget, BaseWidgetProps } from '../BaseWidget';
import { MouseEvent } from 'react';
export interface LinkLayerProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
pointAdded: (point: PointModel, event: MouseEvent) => any;
}
export class LinkLayerWidget extends BaseWidget<LinkLayerProps> {
constructor(props: LinkLayerProps) {
super('srd-link-layer', props);
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<svg
{...this.getProps()}
style={{
transform:
'translate(' +
diagramModel.getOffsetX() +
'px,' +
diagramModel.getOffsetY() +
'px) scale(' +
diagramModel.getZoomLevel() / 100.0 +
')'
}}>
{//only perform these actions when we have a diagram
_.map(diagramModel.getLinks(), link => {
return (
<LinkWidget
pointAdded={this.props.pointAdded}
key={link.getID()}
link={link}
diagramEngine={this.props.diagramEngine}
/>
);
})}
</svg>
);
}
}

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import { DiagramEngine } from '../../DiagramEngine';
import * as _ from 'lodash';
import { NodeWidget } from '../NodeWidget';
import { NodeModel } from '../../models/NodeModel';
import { BaseWidget, BaseWidgetProps } from '../BaseWidget';
export interface NodeLayerProps extends BaseWidgetProps {
diagramEngine: DiagramEngine;
}
export class NodeLayerWidget extends BaseWidget<NodeLayerProps> {
constructor(props: NodeLayerProps) {
super('srd-node-layer', props);
}
render() {
var diagramModel = this.props.diagramEngine.getDiagramModel();
return (
<div
{...this.getProps()}
style={{
transform:
'translate(' +
diagramModel.getOffsetX() +
'px,' +
diagramModel.getOffsetY() +
'px) scale(' +
diagramModel.getZoomLevel() / 100.0 +
')'
}}>
{_.map(diagramModel.getNodes(), (node: NodeModel) => {
return <NodeWidget key={node.getID()} diagramEngine={this.props.diagramEngine} node={node} />;
})}
</div>
);
}
}

13
lib-core/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/@types"
},
"include": [
"./src"
],
"exclude": [
"./dist"
]
}

View File

@@ -0,0 +1,8 @@
const config = require('../webpack.shared')(__dirname);
module.exports = {
...config,
output: {
...config.output,
library: 'projectstorm/react-diagrams-core'
}
};

4
lib-defaults/.npmignore Normal file
View File

@@ -0,0 +1,4 @@
*
!dist/**/*
!package.json
!README.md

3
lib-defaults/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Project STORM > React Diagrams > Defaults
This workspace houses the default models

17
lib-defaults/index.ts Normal file
View File

@@ -0,0 +1,17 @@
export * from './src/label/DefaultLabelFactory';
export * from './src/label/DefaultLabelModel';
export * from './src/label/DefaultLabelWidget';
export * from './src/link/DefaultLinkFactory';
export * from './src/link/DefaultLinkModel';
export * from './src/link/DefaultLinkWidget';
export * from './src/link/DefaultLinkSegmentWidget';
export * from './src/link/DefaultLinkPointWidget';
export * from './src/node/DefaultNodeFactory';
export * from './src/node/DefaultNodeModel';
export * from './src/node/DefaultNodeWidget';
export * from './src/port/DefaultPortFactory';
export * from './src/port/DefaultPortLabelWidget';
export * from './src/port/DefaultPortModel';

40
lib-defaults/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@projectstorm/react-diagrams-defaults",
"version": "6.0.0-alpha.4.2",
"author": "dylanvorster",
"repository": {
"type": "git",
"url": "https://github.com/projectstorm/react-diagrams.git"
},
"scripts": {
"clean": "rm -rf ./dist",
"build": "../node_modules/.bin/webpack",
"build:prod": "NODE_ENV=production ../node_modules/.bin/webpack"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"web",
"diagram",
"diagrams",
"react",
"typescript",
"flowchart",
"simple",
"links",
"nodes"
],
"main": "./dist/index.js",
"typings": "./dist/@types/index",
"dependencies": {
"@projectstorm/react-diagrams-core": "^6.0.0-alpha.4.2"
},
"peerDependencies": {
"@emotion/core": "^10.*",
"@emotion/styled": "^10.*",
"lodash": "4.*",
"react": "16.*"
},
"gitHead": "bb878657ba0c2f81764f32901fd96158a0f8352e"
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
import { AbstractReactFactory } from '@projectstorm/react-diagrams-core';
import { DefaultLabelModel } from './DefaultLabelModel';
import { DefaultLabelWidget } from './DefaultLabelWidget';
/**
* @author Dylan Vorster
*/
export class DefaultLabelFactory extends AbstractReactFactory<DefaultLabelModel> {
constructor() {
super('default');
}
generateReactWidget(event): JSX.Element {
return <DefaultLabelWidget model={event.model} />;
}
generateModel(event): DefaultLabelModel {
return new DefaultLabelModel();
}
}

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