Compare commits
78 Commits
v5.2.2-0
...
v6.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3823adc8e8 | ||
|
|
fce011a11b | ||
|
|
4962343b54 | ||
|
|
45649146c9 | ||
|
|
74e24331ed | ||
|
|
0cc469fa44 | ||
|
|
0b78ba69c6 | ||
|
|
ca301b9d4d | ||
|
|
ad6c77da28 | ||
|
|
66b72b4465 | ||
|
|
bb878657ba | ||
|
|
1db1e21fc2 | ||
|
|
f4920ab4d0 | ||
|
|
ccfd8e026c | ||
|
|
702290d9b3 | ||
|
|
347377d3b4 | ||
|
|
44fd2ad91b | ||
|
|
39d49c9ee0 | ||
|
|
61c6d4b161 | ||
|
|
b324154eff | ||
|
|
17350a740a | ||
|
|
d7fc5370ab | ||
|
|
5fbe7ce9e4 | ||
|
|
3e37e34155 | ||
|
|
f46680596c | ||
|
|
3a2d3ccb73 | ||
|
|
f4aa1852ed | ||
|
|
e167885038 | ||
|
|
619e8a2347 | ||
|
|
4c1e67106a | ||
|
|
d08be537e1 | ||
|
|
8e5243da95 | ||
|
|
22236f8420 | ||
|
|
99fe94ae29 | ||
|
|
3b0c19d998 | ||
|
|
fc67d477a9 | ||
|
|
e4d427e0d3 | ||
|
|
bc2d444aee | ||
|
|
b15a326dab | ||
|
|
b54feb03e4 | ||
|
|
39eddc0a2a | ||
|
|
85a1fde2e5 | ||
|
|
c0dae87d02 | ||
|
|
fddec93370 | ||
|
|
01ffbce479 | ||
|
|
30790682d3 | ||
|
|
dc9c2eae66 | ||
|
|
969152a814 | ||
|
|
2e38a7cfd5 | ||
|
|
e3fe7319c1 | ||
|
|
c3a332ebab | ||
|
|
314bb908f9 | ||
|
|
9b16b3f4ed | ||
|
|
722b6d98f3 | ||
|
|
2c6971be9a | ||
|
|
ea0160cbcf | ||
|
|
efc53eec9b | ||
|
|
b5c86159d1 | ||
|
|
ea2070ec98 | ||
|
|
39c3611686 | ||
|
|
1ff63d4117 | ||
|
|
e80da8853f | ||
|
|
a06e23eaf6 | ||
|
|
4b599fc37f | ||
|
|
def92af022 | ||
|
|
01e9f5f2db | ||
|
|
aa1a02774d | ||
|
|
e60aac56db | ||
|
|
b2e74d8b97 | ||
|
|
e333a4f168 | ||
|
|
1d316409e1 | ||
|
|
9d947d40b7 | ||
|
|
7d80f15358 | ||
|
|
28844b86eb | ||
|
|
70c9aeac66 | ||
|
|
12a7bfbda5 | ||
|
|
650061db4a | ||
|
|
48c6d1f038 |
@@ -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
|
||||
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
@@ -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
|
||||
|
||||
194
.npmignore
@@ -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
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.out
|
||||
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"useTabs": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
102
CHANGELOG.md
@@ -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__
|
||||
|
||||
49
README.md
@@ -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.
|
||||
|
||||
[](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://npmjs.org/package/storm-react-diagrams) [](https://npmjs.org/package/storm-react-diagrams) [](http://packagequality.com/#?package=storm-react-diagrams) [](https://circleci.com/gh/projectstorm/react-diagrams/tree/master)
|
||||
[](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://npmjs.org/package/@projectstorm/react-diagrams) [](https://npmjs.org/package/storm-react-diagrams) [](http://packagequality.com/#?package=storm-react-diagrams) [](https://circleci.com/gh/projectstorm/react-diagrams/tree/master) [](https://lerna.js.org/)
|
||||
|
||||
Example implementation using custom models:
|
||||

|
||||

|
||||
|
||||
Example implementation using custom models: (Dylan's personal code)
|
||||
|
||||

|
||||

|
||||
Get started with the default models right out of the box:
|
||||

|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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()
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"../tslint.json"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": false,
|
||||
"max-classes-per-file": false,
|
||||
"no-var-requires": false
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@@ -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
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "6.0.0-alpha.4.2"
|
||||
}
|
||||
4
lib-all/.npmignore
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
||||
37
lib-all/index.ts
Normal 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
@@ -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
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/@types"
|
||||
},
|
||||
"include": [
|
||||
"./index.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
}
|
||||
8
lib-all/webpack.config.js
Normal 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
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
||||
15
lib-core/CHANGELOG.md
Normal 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
@@ -0,0 +1,3 @@
|
||||
# Project STORM > React Diagrams > Core
|
||||
|
||||
This workspace houses the default models
|
||||
43
lib-core/index.ts
Normal 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
@@ -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"
|
||||
}
|
||||
13
lib-core/sass/_DiagramWidget.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
11
lib-core/sass/_LinkLayerWidget.scss
Normal 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;
|
||||
}
|
||||
11
lib-core/sass/_NodeLayerWidget.scss
Normal 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%;
|
||||
}
|
||||
14
lib-core/sass/_NodeWidget.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib-core/sass/_PortWidget.scss
Normal 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
@@ -0,0 +1,5 @@
|
||||
@import 'DiagramWidget';
|
||||
@import 'LinkLayerWidget';
|
||||
@import 'NodeLayerWidget';
|
||||
@import 'NodeWidget';
|
||||
@import 'PortWidget';
|
||||
336
lib-core/src/DiagramEngine.ts
Normal 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
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
28
lib-core/src/actions/move-canvas/MoveCanvasAction.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
17
lib-core/src/actions/move-canvas/MoveCanvasActionFactory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
177
lib-core/src/actions/move-items/MoveItemsAction.ts
Normal 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()
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
32
lib-core/src/actions/move-items/MoveItemsActionFactory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
72
lib-core/src/actions/selecting-items/SelectingAction.ts
Normal 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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
12
lib-core/src/core-actions/AbstractAction.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
16
lib-core/src/core-actions/AbstractActionFactory.ts
Normal 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;
|
||||
}
|
||||
21
lib-core/src/core-actions/AbstractMouseAction.ts
Normal 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);
|
||||
}
|
||||
105
lib-core/src/core-models/BaseEntity.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib-core/src/core-models/BaseModel.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
62
lib-core/src/core-models/BasePositionModel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
34
lib-core/src/core/AbstractFactory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
13
lib-core/src/core/AbstractModelFactory.ts
Normal 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;
|
||||
}
|
||||
16
lib-core/src/core/AbstractReactFactory.tsx
Normal 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;
|
||||
}
|
||||
144
lib-core/src/core/BaseObserver.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
59
lib-core/src/core/FactoryBank.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
285
lib-core/src/models/DiagramModel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
37
lib-core/src/models/LabelModel.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
146
lib-core/src/models/NodeModel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
56
lib-core/src/models/PointModel.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
143
lib-core/src/models/PortModel.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
7
lib-core/src/models/SelectionModel.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseModel } from '../core-models/BaseModel';
|
||||
|
||||
export interface SelectionModel {
|
||||
model: BaseModel;
|
||||
initialX: number;
|
||||
initialY: number;
|
||||
}
|
||||
1
lib-core/src/widgets/ActionManager.ts
Normal file
@@ -0,0 +1 @@
|
||||
export class ActionManager {}
|
||||
@@ -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 {
|
||||
288
lib-core/src/widgets/DiagramWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib-core/src/widgets/LabelWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib-core/src/widgets/LinkWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib-core/src/widgets/NodeWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib-core/src/widgets/PeformanceWidget.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
86
lib-core/src/widgets/PortWidget.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib-core/src/widgets/layers/LinkLayerWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib-core/src/widgets/layers/NodeLayerWidget.tsx
Normal 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
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/@types"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
}
|
||||
8
lib-core/webpack.config.js
Normal 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
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
||||
3
lib-defaults/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Project STORM > React Diagrams > Defaults
|
||||
|
||||
This workspace houses the default models
|
||||
17
lib-defaults/index.ts
Normal 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
@@ -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"
|
||||
}
|
||||
21
lib-defaults/src/label/DefaultLabelFactory.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||