mirror of
https://github.com/projectstorm/react-diagrams.git
synced 2026-03-13 09:50:09 +08:00
Compare commits
42 Commits
react_canv
...
v5.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e333a4f168 | ||
|
|
1d316409e1 | ||
|
|
9d947d40b7 | ||
|
|
7d80f15358 | ||
|
|
28844b86eb | ||
|
|
70c9aeac66 | ||
|
|
12a7bfbda5 | ||
|
|
650061db4a | ||
|
|
48c6d1f038 | ||
|
|
7cea7032f0 | ||
|
|
2b346aa326 | ||
|
|
0f7f8853d1 | ||
|
|
3a57ea711e | ||
|
|
de208e3250 | ||
|
|
2df09edb7e | ||
|
|
ba1401a8f2 | ||
|
|
c6137d03ce | ||
|
|
ead43957e3 | ||
|
|
b7d8ffa793 | ||
|
|
a289c73b68 | ||
|
|
b75976d091 | ||
|
|
a95010652d | ||
|
|
4cd24940c8 | ||
|
|
0f1dec1ebc | ||
|
|
eedc21cc87 | ||
|
|
b719178f44 | ||
|
|
12724748e6 | ||
|
|
0ebd6d918f | ||
|
|
591ec2e76e | ||
|
|
9f95ac8451 | ||
|
|
e3fc996092 | ||
|
|
de9765358f | ||
|
|
fa34f5c98b | ||
|
|
f4ac467056 | ||
|
|
5b142c1c61 | ||
|
|
40219d7a4d | ||
|
|
0eb432b302 | ||
|
|
ad5e7a4b4d | ||
|
|
0583db1c74 | ||
|
|
0b92e939ab | ||
|
|
8997eba4b4 | ||
|
|
3fbbeb82de |
@@ -1,6 +1,6 @@
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Some exceptions
|
||||
|
||||
200
.gitignore
vendored
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
|
||||
|
||||
198
.npmignore
198
.npmignore
@@ -1,194 +1,4 @@
|
||||
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
|
||||
*
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
||||
|
||||
14
.storybook/addon-code/react.js
vendored
14
.storybook/addon-code/react.js
vendored
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
|
||||
export class WithCode extends React.Component {
|
||||
render() {
|
||||
const { children, code } = this.props;
|
||||
const channel = addons.getChannel();
|
||||
|
||||
// send the notes to the channel.
|
||||
channel.emit('storybook/code/set_code', code);
|
||||
// return children elements.
|
||||
return children;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/styles/hljs';
|
||||
|
||||
/**
|
||||
* @author Dylan
|
||||
*
|
||||
* Simple little addon for displaying code, might make this a seperate project at some point
|
||||
*/
|
||||
export class CodePreview extends React.Component {
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = {
|
||||
code: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { channel, api } = this.props;
|
||||
// Listen to the notes and render it.
|
||||
channel.on('storybook/code/set_code', (code) => {
|
||||
this.onAddCode(code);
|
||||
});
|
||||
|
||||
// Clear the current notes on every story change.
|
||||
this.stopListeningOnStory = api.onStory(() => {
|
||||
this.onAddCode('');
|
||||
});
|
||||
}
|
||||
|
||||
// This is some cleanup tasks when the Notes panel is unmounting.
|
||||
componentWillUnmount() {
|
||||
if (this.stopListeningOnStory) {
|
||||
this.stopListeningOnStory();
|
||||
}
|
||||
|
||||
this.unmounted = true;
|
||||
const { channel } = this.props;
|
||||
channel.removeListener('storybook/notes/add_notes', this.onAddCode);
|
||||
}
|
||||
|
||||
onAddCode(code) {
|
||||
this.setState({ code: code });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
customStyle={{width: '100%', overflowX:'hidden', tabSize: 4}}
|
||||
showLineNumbers={true}
|
||||
language='language-tsx'
|
||||
style={github}
|
||||
>
|
||||
{this.state.code}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the addon with a unique name.
|
||||
addons.register('storybook/code', api => {
|
||||
// Also need to set a unique name to the panel.
|
||||
addons.addPanel('storybook/code/panel', {
|
||||
title: 'Code preview',
|
||||
render: () => <CodePreview channel={addons.getChannel()} api={api} />,
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,2 @@
|
||||
import './addon-code/register';
|
||||
import '@storybook/addon-actions/register';
|
||||
import '@storybook/addon-options/register';
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
const path = require('path');
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loaders: ["style-loader", "css-loader", "sass-loader"],
|
||||
include: path.resolve(__dirname, '../')
|
||||
},
|
||||
{
|
||||
test: /\.css/,
|
||||
loaders: ["style-loader", "css-loader"],
|
||||
include: path.resolve(__dirname, '../')
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
loader: "source-map-loader",
|
||||
exclude: [
|
||||
/node_modules\//
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'awesome-typescript-loader?declaration=false',
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
|
||||
loader: "file-loader"
|
||||
module.exports = async ({config, mode}) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
'storm-react-diagrams': path.join(__dirname, "..", "src", "main")
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'storm-react-diagrams': path.join(__dirname, "..", "src", "main")
|
||||
},
|
||||
extensions: [".tsx", ".ts", ".js"]
|
||||
}
|
||||
module: {
|
||||
...config.module,
|
||||
rules: [
|
||||
...config.module.rules,
|
||||
...[
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loaders: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {config: {path: path.join(__dirname, '..')}}
|
||||
},
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
loader: 'source-map-loader',
|
||||
exclude: [/node_modules\//]
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true
|
||||
}
|
||||
},
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,22 @@
|
||||
__5.3.0__
|
||||
|
||||
* [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
|
||||
-> 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
|
||||
|
||||
21
README.md
21
README.md
@@ -1,12 +1,14 @@
|
||||
# STORM React Diagrams
|
||||
|
||||
**PSA**: React Diagrams is currently getting a bit of a rewrite to enable much more advanced features. To see the new foundation WIP visit [https://github.com/projectstorm/react-canvas](https://github.com/projectstorm/react-canvas).
|
||||
__PSA 2018__: React Diagrams ~is currently~ was getting a bit of a rewrite to enable much more advanced features. To see the new foundation WIP visit [https://github.com/projectstorm/react-canvas](https://github.com/projectstorm/react-canvas).
|
||||
|
||||
__PSA 2019__: I still want to jump onto the rewrite, but it is a much larger project than anticipated, so going to try maintain this one in the mean time.
|
||||
|
||||
---
|
||||
|
||||
**DEMO**: [http://www.projectstorm.io/react-diagrams](http://www.projectstorm.io/react-diagrams)
|
||||
**DEMO**: [http://projectstorm.cloud/react-diagrams](http://projectstorm.cloud/react-diagrams)
|
||||
|
||||
**DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
|
||||
**(SOME) DOCS:** [https://projectstorm.gitbooks.io/react-diagrams](https://projectstorm.gitbooks.io/react-diagrams)
|
||||
|
||||
**RELEASE NOTES** : [http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/](http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/)
|
||||
|
||||
@@ -14,12 +16,13 @@ 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)
|
||||
|
||||
Example implementation using custom models:
|
||||

|
||||
|
||||

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

|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
A no-nonsense diagramming library written entirely in React with the help of a few small libraries. It aims to be:
|
||||
@@ -29,7 +32,11 @@ A no-nonsense diagramming library written entirely in React with the help of a f
|
||||
* Simple to operate and understand without sugar and magic
|
||||
* Fast and optimized to handle large diagrams with hundreds of nodes/links
|
||||
* Super easy to use, and should work as you expect it to
|
||||
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages
|
||||
* Perfect for creating declarative systems such as programmatic pipelines and visual programming languages
|
||||
|
||||
#### Installing
|
||||
|
||||
```yarn add @projectstorm/react-diagrams```
|
||||
|
||||
#### Run the demos
|
||||
|
||||
@@ -37,7 +44,7 @@ After running `yarn install` you must then run: `yarn run storybook`
|
||||
|
||||
#### 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 `webpack` in the root directory \(or `export NODE_ENV=production && webpack` if you want a production build\) and it will spit out the transpiled code and typescript definitions into the dist directory as a single file.
|
||||
We use webpack for this because TSC cannot compile a single UMD file \(TSC can currently only output multiple UMD files\).
|
||||
|
||||
## [Checkout the docs](https://projectstorm.gitbooks.io/react-diagrams)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { withDocs } from "storybook-readme";
|
||||
import { WithCode } from "../../.storybook/addon-code/react.js";
|
||||
|
||||
export class Helper {
|
||||
/**
|
||||
@@ -22,16 +20,4 @@ export class Helper {
|
||||
console.log(event.clientX, event.clientY);
|
||||
});
|
||||
}
|
||||
|
||||
static makeDemo(widget, code, markdown?) {
|
||||
let container = () => <WithCode code={code}>{widget}</WithCode>;
|
||||
if (markdown) {
|
||||
return withDocs({
|
||||
PreviewComponent: ({ children }) => {
|
||||
return <div className="docs-preview-wrapper">{children}</div>;
|
||||
}
|
||||
})(markdown, container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
@import "../../src/sass/main";
|
||||
|
||||
html, body, #root{
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.srd-demo-workspace{
|
||||
background: black;
|
||||
display: flex;
|
||||
@@ -36,15 +42,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.docs-preview-wrapper{
|
||||
background: rgb(60,60,60);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.srd-demo-canvas{
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
@@ -59,7 +59,7 @@ class CloneSelected extends React.Component<any, any> {
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -81,7 +81,7 @@ export default () => {
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <CloneSelected engine={engine} model={model} />;
|
||||
|
||||
@@ -118,7 +118,7 @@ export class AdvancedLinkFactory extends DefaultLinkFactory {
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
engine.registerLinkFactory(new AdvancedLinkFactory());
|
||||
|
||||
// create some nodes
|
||||
@@ -150,7 +150,7 @@ export default () => {
|
||||
model.addAll(node1, node2, node3, node4);
|
||||
|
||||
// load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
// render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { DiamondPortModel } from "./DiamondPortModel";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
// register some other factories as well
|
||||
engine.registerPortFactory(new SimplePortFactory("diamond", config => new DiamondPortModel()));
|
||||
@@ -50,7 +50,7 @@ export default () => {
|
||||
model.addAll(node1, node2, node3, link1, link2);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
|
||||
|
||||
@@ -11,8 +11,8 @@ export function distributeElements(model) {
|
||||
let nodes = distributeGraph(clonedModel);
|
||||
nodes.forEach(node => {
|
||||
let modelNode = clonedModel.nodes.find(item => item.id === node.id);
|
||||
modelNode.x = node.x;
|
||||
modelNode.y = node.y;
|
||||
modelNode.x = node.x - node.width / 2;
|
||||
modelNode.y = node.y - node.height / 2;
|
||||
});
|
||||
return clonedModel;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Demo8Widget extends React.Component<any, any> {
|
||||
const { engine } = this.props;
|
||||
const model = engine.getDiagramModel();
|
||||
let distributedModel = getDistributedModel(engine, model);
|
||||
engine.setModel(distributedModel);
|
||||
engine.setDiagramModel(distributedModel);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function getDistributedModel(engine, model) {
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
let engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
let model = new DiagramModel();
|
||||
@@ -114,7 +114,7 @@ export default () => {
|
||||
//5) load model into engine
|
||||
let model2 = getDistributedModel(engine, model);
|
||||
|
||||
engine.setModel(model2);
|
||||
engine.setDiagramModel(model2);
|
||||
|
||||
return <Demo8Widget engine={engine} />;
|
||||
};
|
||||
|
||||
@@ -9,14 +9,14 @@ export class Application {
|
||||
|
||||
constructor() {
|
||||
this.diagramEngine = new SRD.DiagramEngine();
|
||||
this.diagramEngine.installDefaults();
|
||||
this.diagramEngine.installDefaultFactories();
|
||||
|
||||
this.newModel();
|
||||
}
|
||||
|
||||
public newModel() {
|
||||
this.activeModel = new SRD.DiagramModel();
|
||||
this.diagramEngine.setModel(this.activeModel);
|
||||
this.diagramEngine.setDiagramModel(this.activeModel);
|
||||
|
||||
//3-A) create a default node
|
||||
var node1 = new SRD.DefaultNodeModel("Node 1", "rgb(0,192,255)");
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as React from "react";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -30,7 +30,7 @@ export default () => {
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { action } from "@storybook/addon-actions";
|
||||
export default () => {
|
||||
// setup the diagram engine
|
||||
const engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
// setup the diagram model
|
||||
const model = new DiagramModel();
|
||||
@@ -46,14 +46,13 @@ export default () => {
|
||||
|
||||
// also a label for A and D
|
||||
const link3 = port1.link(port4);
|
||||
link3.setTargetPort(port4);
|
||||
(link3 as DefaultLinkModel).addLabel("Emoji label: 🎉");
|
||||
|
||||
// add all to the main model
|
||||
model.addAll(node1, node2, node3, node4, link1, link2, link3);
|
||||
|
||||
// load model into engine and render
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
return (
|
||||
<DemoWorkspaceWidget
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export default () => {
|
||||
// setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
var model = new DiagramModel();
|
||||
|
||||
@@ -33,7 +33,7 @@ export default () => {
|
||||
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
var props = {
|
||||
diagramEngine: engine,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
export default () => {
|
||||
// setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
var model = new DiagramModel();
|
||||
|
||||
@@ -46,7 +46,7 @@ export default () => {
|
||||
});
|
||||
});
|
||||
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
var props = {
|
||||
diagramEngine: engine,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
var model = new DiagramModel();
|
||||
|
||||
@@ -51,7 +51,7 @@ export default () => {
|
||||
|
||||
model.addAll(node3, node4, link2);
|
||||
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//!========================================= <<<<<<<
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
|
||||
node.x += 30;
|
||||
node.y += 30;
|
||||
model2.deSerializeDiagram(obj, engine);
|
||||
engine.setModel(model2);
|
||||
engine.setDiagramModel(model2);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class NodeDelayedPosition extends React.Component<any, any> {
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -79,7 +79,7 @@ export default () => {
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <NodeDelayedPosition engine={engine} model={model} />;
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as React from "react";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -23,7 +23,7 @@ export default () => {
|
||||
}
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel } from "storm-react-diagrams";
|
||||
import { DiagramEngine, DiagramModel, DefaultNodeModel, LinkModel, DiagramWidget } from "storm-react-diagrams";
|
||||
import * as React from "react";
|
||||
import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import * as beautify from "json-beautify";
|
||||
import {DeserializeEvent} from "../../../react-canvas/src/models/BaseModel";
|
||||
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -30,17 +29,17 @@ export default () => {
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//!------------- SERIALIZING ------------------
|
||||
|
||||
var str = JSON.stringify(model.serialize());
|
||||
var str = JSON.stringify(model.serializeDiagram());
|
||||
|
||||
//!------------- DESERIALIZING ----------------
|
||||
|
||||
var model2 = new DiagramModel();
|
||||
model2.deSerialize(new DeserializeEvent(JSON.parse(str), engine));
|
||||
engine.setModel(model2);
|
||||
model2.deSerializeDiagram(JSON.parse(str), engine);
|
||||
engine.setDiagramModel(model2);
|
||||
|
||||
return (
|
||||
<DemoWorkspaceWidget
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -31,7 +31,7 @@ export default () => {
|
||||
model.addAll(node1, node2, node3, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} allowLooseLinks={false} />;
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as React from "react";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -34,7 +34,7 @@ export default () => {
|
||||
model.addAll(node1, node2, link1);
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return <DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { action } from "@storybook/addon-actions";
|
||||
export default () => {
|
||||
// setup the diagram engine
|
||||
const engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
// setup the diagram model
|
||||
const model = new DiagramModel();
|
||||
@@ -43,7 +43,7 @@ export default () => {
|
||||
model.addAll(node1, node2, node3, node4, node5, link1, link2);
|
||||
|
||||
// load model into engine and render
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
return (
|
||||
<DemoWorkspaceWidget
|
||||
|
||||
@@ -18,7 +18,7 @@ import { DemoWorkspaceWidget } from "../.helpers/DemoWorkspaceWidget";
|
||||
export default () => {
|
||||
//1) setup the diagram engine
|
||||
var engine = new DiagramEngine();
|
||||
engine.installDefaults();
|
||||
engine.installDefaultFactories();
|
||||
|
||||
//2) setup the diagram model
|
||||
var model = new DiagramModel();
|
||||
@@ -30,13 +30,11 @@ export default () => {
|
||||
}
|
||||
|
||||
//5) load model into engine
|
||||
engine.setModel(model);
|
||||
engine.setDiagramModel(model);
|
||||
|
||||
//6) render the diagram!
|
||||
return (
|
||||
<DemoWorkspaceWidget
|
||||
buttons={<button onClick={() => engine.getCanvasWidget().zoomToFit()}>Zoom to fit</button>}
|
||||
>
|
||||
<DemoWorkspaceWidget buttons={<button onClick={() => engine.zoomToFit()}>Zoom to fit</button>}>
|
||||
<DiagramWidget className="srd-demo-canvas" diagramEngine={engine} />
|
||||
</DemoWorkspaceWidget>
|
||||
);
|
||||
|
||||
172
demos/index.tsx
172
demos/index.tsx
@@ -1,23 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { storiesOf, addDecorator } from "@storybook/react";
|
||||
import { setOptions } from "@storybook/addon-options";
|
||||
import { host } from "storybook-host";
|
||||
import { Helper } from "./.helpers/Helper";
|
||||
import { Toolkit } from "../src/Toolkit";
|
||||
|
||||
//include the SCSS for the demo
|
||||
import "./.helpers/demo.scss";
|
||||
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';
|
||||
|
||||
Toolkit.TESTING = true;
|
||||
|
||||
addDecorator(
|
||||
host({
|
||||
cropMarks: false,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
padding: 20
|
||||
})
|
||||
);
|
||||
addParameters({
|
||||
options: {
|
||||
theme: themes.dark,
|
||||
},
|
||||
});
|
||||
|
||||
//include the SCSS for the demo
|
||||
import "./.helpers/demo.scss";
|
||||
|
||||
setOptions({
|
||||
name: "STORM React Diagrams",
|
||||
@@ -25,105 +23,51 @@ setOptions({
|
||||
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",
|
||||
Helper.makeDemo(
|
||||
require("./demo-simple/index").default(),
|
||||
require("!!raw-loader!./demo-simple/index"),
|
||||
require("./demo-simple/docs.md")
|
||||
)
|
||||
)
|
||||
// .add(
|
||||
// "Simple flow example",
|
||||
// Helper.makeDemo(require("./demo-simple-flow/index").default(), require("!!raw-loader!./demo-simple-flow/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Performance demo",
|
||||
// Helper.makeDemo(require("./demo-performance/index").default(), require("!!raw-loader!./demo-performance/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Locked widget",
|
||||
// Helper.makeDemo(require("./demo-locks/index").default(), require("!!raw-loader!./demo-locks/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Canvas grid size",
|
||||
// Helper.makeDemo(require("./demo-grid/index").default(), require("!!raw-loader!./demo-grid/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Limiting link points",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-limit-points/index").default(),
|
||||
// require("!!raw-loader!./demo-limit-points/index")
|
||||
// )
|
||||
// )
|
||||
// .add(
|
||||
// "Events and listeners",
|
||||
// Helper.makeDemo(require("./demo-listeners/index").default(), require("!!raw-loader!./demo-listeners/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Zoom to fit",
|
||||
// Helper.makeDemo(require("./demo-zoom-to-fit/index").default(), require("!!raw-loader!./demo-zoom-to-fit/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Links with labels",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-labelled-links/index").default(),
|
||||
// require("!!raw-loader!./demo-labelled-links/index")
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// storiesOf("Advanced Techniques", module)
|
||||
// .add(
|
||||
// "Clone Selected",
|
||||
// Helper.makeDemo(require("./demo-cloning/index").default(), require("!!raw-loader!./demo-cloning/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Serializing and de-serializing",
|
||||
// Helper.makeDemo(require("./demo-serializing/index").default(), require("!!raw-loader!./demo-serializing/index"))
|
||||
// )
|
||||
// .add(
|
||||
// "Programatically modifying graph",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-mutate-graph/index").default(),
|
||||
// require("!!raw-loader!./demo-mutate-graph/index")
|
||||
// )
|
||||
// )
|
||||
// .add(
|
||||
// "Large application",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-drag-and-drop/index").default(),
|
||||
// require("!!raw-loader!./demo-drag-and-drop/components/BodyWidget")
|
||||
// )
|
||||
// )
|
||||
// .add(
|
||||
// "Smart routing",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-smart-routing/index").default(),
|
||||
// require("!!raw-loader!./demo-smart-routing/index")
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// storiesOf("Custom Models", module)
|
||||
// .add(
|
||||
// "Custom diamond node",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-custom-node1/index").default(),
|
||||
// require("!!raw-loader!./demo-custom-node1/index")
|
||||
// )
|
||||
// )
|
||||
// .add(
|
||||
// "Custom animated links",
|
||||
// Helper.makeDemo(
|
||||
// require("./demo-custom-link1/index").default(),
|
||||
// require("!!raw-loader!./demo-custom-link1/index")
|
||||
// )
|
||||
// );
|
||||
//
|
||||
// storiesOf("3rd party libraries", module).add(
|
||||
// "Auto Distribute (Dagre)",
|
||||
// Helper.makeDemo(require("./demo-dagre/index").default(), require("!!raw-loader!./demo-dagre/index"))
|
||||
// );
|
||||
.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()
|
||||
|
||||
@@ -7,7 +7,7 @@ The first thing you need to do, is grab the distribution files on NPM. You can d
|
||||
**Via yarn:**
|
||||
|
||||
```
|
||||
yarn install storm-react-diagrams
|
||||
yarn add storm-react-diagrams
|
||||
```
|
||||
|
||||
**Via npm:**
|
||||
|
||||
@@ -26,8 +26,8 @@ tell the mouse pointer to click and drag on various elements while making assert
|
||||
|
||||
We use Jest for the assertions and the interactivity is handled by puppeteer. Due to the laborious nature
|
||||
of writing e2e tests, there is a helper method that is provided in each test that makes interacting
|
||||
with the diagrams a lot easier. Using this helper, you cna easily tell the mouse to drag links between nodes,
|
||||
with the diagrams a lot easier. Using this helper, you can easily tell the mouse to drag links between nodes,
|
||||
select them and also easily assert information about them. The important thing here, is that this helper
|
||||
does not touch the model in any way, but is purely a helper for writing the tests themselves. Please
|
||||
make use of this helper when writing tests, as it ensure that the tests are defensive in nature, and also
|
||||
reduces the overhead of physically writing them.
|
||||
reduces the overhead of physically writing them.
|
||||
|
||||
7
jest-puppeteer.config.js
Normal file
7
jest-puppeteer.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
launch: {
|
||||
dumpio: true,
|
||||
headless: process.env.CI === 'true',
|
||||
},
|
||||
browserContext: 'default',
|
||||
}
|
||||
@@ -1,27 +1,20 @@
|
||||
const path = require("path");
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
moduleFileExtensions: [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"preset": "jest-puppeteer",
|
||||
transform: {
|
||||
".*test_loader.*": path.join(__dirname, "tests", "helpers", "storybook-loader.js" ),
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.jsx?$': 'babel-jest'
|
||||
},
|
||||
moduleNameMapper:{
|
||||
"\\.(scss|css|png)$": path.join(__dirname,"tests","helpers","css-mock.js"),
|
||||
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"
|
||||
"**/*\.test\.ts"
|
||||
]
|
||||
};
|
||||
|
||||
114
package.json
114
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "storm-react-diagrams",
|
||||
"version": "5.1.1",
|
||||
"name": "@projectstorm/react-diagrams",
|
||||
"version": "5.3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/projectstorm/react-diagrams.git"
|
||||
@@ -17,7 +17,7 @@
|
||||
"nodes"
|
||||
],
|
||||
"main": "./dist/main.js",
|
||||
"typings": "./dist/@types/src/main",
|
||||
"typings": "./dist/@types/main",
|
||||
"author": "dylanvorster",
|
||||
"scripts": {
|
||||
"build:ts": "webpack",
|
||||
@@ -28,61 +28,71 @@
|
||||
"storybook:github": "storybook-to-ghpages",
|
||||
"pretty": "prettier --use-tabs --write \"{src,demos,tests}/**/*.{ts,tsx}\" --print-width 120",
|
||||
"lint": "tslint -p .",
|
||||
"test:ci": "rm -rf ./dist && node ./tests/e2e/generate-e2e.js && jest --no-cache",
|
||||
"test:ci": "rm -rf ./dist && node ./tests/e2e/generate-e2e.js && jest --runInBand --no-cache",
|
||||
"test": "jest --no-cache",
|
||||
"prepublishOnly": "yarn run build:ts:prod && yarn run build:sass:prod"
|
||||
"prepublishOnly": "rm -rf ./dist && yarn run build:ts:prod && yarn run build:sass:prod"
|
||||
},
|
||||
"dependencies": {
|
||||
"peerDependencies": {
|
||||
"closest": "^0.0.1",
|
||||
"lodash": "^4.17.5",
|
||||
"lodash": "4.*",
|
||||
"pathfinding": "^0.4.18",
|
||||
"paths-js": "^0.4.7",
|
||||
"react": "^16.2.0"
|
||||
"paths-js": "^0.4.9",
|
||||
"react": "16.*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^3.3.15",
|
||||
"@storybook/addon-options": "^3.3.15",
|
||||
"@storybook/addon-storyshots": "^3.3.15",
|
||||
"@storybook/addons": "^3.3.15",
|
||||
"@storybook/react": "^3.3.15",
|
||||
"@storybook/storybook-deployer": "^2.3.0",
|
||||
"@types/jest": "^22.2.0",
|
||||
"@types/lodash": "^4.14.104",
|
||||
"@types/node": "^9.4.7",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@storybook/addon-actions": "^5.1.9",
|
||||
"@storybook/addon-options": "^5.1.9",
|
||||
"@storybook/addon-storyshots": "^5.1.9",
|
||||
"@storybook/addons": "^5.1.9",
|
||||
"@storybook/react": "^5.1.9",
|
||||
"@storybook/storybook-deployer": "^2.8.1",
|
||||
"@storybook/theming": "^5.1.9",
|
||||
"@types/jest": "^24.0.15",
|
||||
"@types/jest-environment-puppeteer": "^4.0.0",
|
||||
"@types/lodash": "^4.14.136",
|
||||
"@types/node": "^12.6.8",
|
||||
"@types/promise": "^7.1.30",
|
||||
"@types/puppeteer": "^1.1.0",
|
||||
"@types/react": "^16.0.40",
|
||||
"awesome-typescript-loader": "^4.0.1",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.10",
|
||||
"dagre": "^0.8.2",
|
||||
"enzyme": "^3.3.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"glob": "^7.1.2",
|
||||
"jest": "^22.4.2",
|
||||
"jest-cli": "^22.4.2",
|
||||
"json-beautify": "^1.0.1",
|
||||
"node-sass": "^4.7.2",
|
||||
"prettier": "^1.11.1",
|
||||
"puppeteer": "^1.1.1",
|
||||
"raf": "^3.4.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-syntax-highlighter": "^7.0.2",
|
||||
"react-test-renderer": "^16.2.0",
|
||||
"sass-loader": "^6.0.7",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"storybook-host": "^4.1.5",
|
||||
"storybook-readme": "^3.2.1",
|
||||
"style-loader": "^0.20.3",
|
||||
"ts-jest": "^22.4.1",
|
||||
"tslint": "^5.9.1",
|
||||
"ts-loader": "^4.1.0",
|
||||
"typescript": "^2.7.2",
|
||||
"uglifyjs-webpack-plugin": "^1.2.3",
|
||||
"val-loader": "^1.1.0",
|
||||
"webpack": "^4.1.1",
|
||||
"webpack-cli": "^2.0.11"
|
||||
"@types/puppeteer": "^1.12.4",
|
||||
"@types/react": "^16.8.23",
|
||||
"babel-jest": "^24.8.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"closest": "^0.0.1",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.1.0",
|
||||
"dagre": "^0.8.4",
|
||||
"enzyme": "^3.10.0",
|
||||
"file-loader": "^4.1.0",
|
||||
"glob": "^7.1.4",
|
||||
"jest": "^24.8.0",
|
||||
"jest-cli": "^24.8.0",
|
||||
"jest-puppeteer": "^4.3.0",
|
||||
"json-beautify": "^1.1.0",
|
||||
"lodash": "4.*",
|
||||
"node-sass": "^4.12.0",
|
||||
"pathfinding": "^0.4.18",
|
||||
"paths-js": "^0.4.9",
|
||||
"prettier": "^1.18.2",
|
||||
"puppeteer": "^1.18.1",
|
||||
"raf": "^3.4.1",
|
||||
"raw-loader": "^3.1.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-syntax-highlighter": "^11.0.2",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"sass-loader": "^7.1.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"storybook-host": "^5.1.0",
|
||||
"storybook-readme": "^5.0.5",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.3.0",
|
||||
"ts-jest": "^24.0.2",
|
||||
"ts-loader": "^6.0.4",
|
||||
"typescript": "^3.5.3",
|
||||
"val-loader": "^1.1.1",
|
||||
"webpack": "^4.36.1",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
3
postcss.config.js
Normal file
3
postcss.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require('autoprefixer')]
|
||||
};
|
||||
115
src/BaseEntity.ts
Normal file
115
src/BaseEntity.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Toolkit } from "./Toolkit";
|
||||
import * as _ from "lodash";
|
||||
import { DiagramEngine } from "./DiagramEngine";
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export interface BaseEvent<T extends BaseEntity = any> {
|
||||
entity: BaseEntity<BaseListener>;
|
||||
stopPropagation: () => any;
|
||||
firing: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface BaseListener<T extends BaseEntity = any> {
|
||||
lockChanged?(event: BaseEvent<T> & { locked: boolean }): void;
|
||||
}
|
||||
|
||||
export type BaseEntityType = "node" | "link" | "port" | "point";
|
||||
|
||||
export class BaseEntity<T extends BaseListener = BaseListener> {
|
||||
public listeners: { [s: string]: T };
|
||||
public id: string;
|
||||
public locked: boolean;
|
||||
|
||||
constructor(id?: string) {
|
||||
this.listeners = {};
|
||||
this.id = id || Toolkit.UID();
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
getID() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
doClone(lookupTable: { [s: string]: any } = {}, clone: any) {
|
||||
/*noop*/
|
||||
}
|
||||
|
||||
clone(lookupTable: { [s: string]: any } = {}) {
|
||||
// try and use an existing clone first
|
||||
if (lookupTable[this.id]) {
|
||||
return lookupTable[this.id];
|
||||
}
|
||||
let clone = _.clone(this);
|
||||
clone.id = Toolkit.UID();
|
||||
clone.clearListeners();
|
||||
lookupTable[this.id] = clone;
|
||||
|
||||
this.doClone(lookupTable, clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
clearListeners() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
public deSerialize(data: { [s: string]: any }, engine: DiagramEngine) {
|
||||
this.id = data.id;
|
||||
}
|
||||
|
||||
public serialize() {
|
||||
return {
|
||||
id: this.id
|
||||
};
|
||||
}
|
||||
|
||||
public iterateListeners(cb: (t: T, event: BaseEvent) => any) {
|
||||
let event: BaseEvent = {
|
||||
id: Toolkit.UID(),
|
||||
firing: true,
|
||||
entity: this,
|
||||
stopPropagation: () => {
|
||||
event.firing = false;
|
||||
}
|
||||
};
|
||||
|
||||
for (var i in this.listeners) {
|
||||
if (this.listeners.hasOwnProperty(i)) {
|
||||
// propagation stopped
|
||||
if (!event.firing) {
|
||||
return;
|
||||
}
|
||||
cb(this.listeners[i], event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeListener(listener: string) {
|
||||
if (this.listeners[listener]) {
|
||||
delete this.listeners[listener];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public addListener(listener: T): string {
|
||||
var uid = Toolkit.UID();
|
||||
this.listeners[uid] = listener;
|
||||
return uid;
|
||||
}
|
||||
|
||||
public isLocked(): boolean {
|
||||
return this.locked;
|
||||
}
|
||||
|
||||
public setLocked(locked: boolean = true) {
|
||||
this.locked = locked;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.lockChanged) {
|
||||
listener.lockChanged({ ...event, locked: locked });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,365 @@
|
||||
import { BaseEntity, BaseListener } from "./BaseEntity";
|
||||
import { DiagramModel } from "./models/DiagramModel";
|
||||
import { CanvasEngine } from "@projectstorm/react-canvas";
|
||||
import { DefaultLabelFactory, DefaultLinkFactory, DefaultNodeFactory, DefaultPortFactory } from "storm-react-diagrams";
|
||||
import * as _ from "lodash";
|
||||
import { BaseModel, BaseModelListener } from "./models/BaseModel";
|
||||
import { NodeModel } from "./models/NodeModel";
|
||||
import { PointModel } from "./models/PointModel";
|
||||
import { PortModel } from "./models/PortModel";
|
||||
import { LinkModel } from "./models/LinkModel";
|
||||
import { AbstractLabelFactory } from "./factories/AbstractLabelFactory";
|
||||
import { AbstractLinkFactory } from "./factories/AbstractLinkFactory";
|
||||
import { AbstractNodeFactory } from "./factories/AbstractNodeFactory";
|
||||
import { AbstractPortFactory } from "./factories/AbstractPortFactory";
|
||||
import { DefaultLinkFactory, DefaultNodeFactory } from "./main";
|
||||
import { ROUTING_SCALING_FACTOR } from "./routing/PathFinding";
|
||||
import { DefaultPortFactory } from "./defaults/factories/DefaultPortFactory";
|
||||
import { LabelModel } from "./models/LabelModel";
|
||||
import { DefaultLabelFactory } from "./defaults/factories/DefaultLabelFactory";
|
||||
import { Toolkit } from "./Toolkit";
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export interface DiagramEngineListener extends BaseListener {
|
||||
portFactoriesUpdated?(): void;
|
||||
|
||||
export class DiagramEngine extends CanvasEngine<DiagramModel> {
|
||||
nodeFactoriesUpdated?(): void;
|
||||
|
||||
linkFactoriesUpdated?(): void;
|
||||
|
||||
labelFactoriesUpdated?(): void;
|
||||
|
||||
repaintCanvas?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed as a parameter to the DiagramWidget
|
||||
*/
|
||||
export class DiagramEngine extends BaseEntity<DiagramEngineListener> {
|
||||
nodeFactories: { [s: string]: AbstractNodeFactory };
|
||||
linkFactories: { [s: string]: AbstractLinkFactory };
|
||||
portFactories: { [s: string]: AbstractPortFactory };
|
||||
labelFactories: { [s: string]: AbstractLabelFactory };
|
||||
|
||||
diagramModel: DiagramModel;
|
||||
canvas: Element;
|
||||
paintableWidgets: {};
|
||||
linksThatHaveInitiallyRendered: {};
|
||||
nodesRendered: boolean;
|
||||
maxNumberPointsPerLink: number;
|
||||
smartRouting: boolean;
|
||||
|
||||
// calculated only when smart routing is active
|
||||
canvasMatrix: number[][] = [];
|
||||
routingMatrix: number[][] = [];
|
||||
// used when at least one element has negative coordinates
|
||||
hAdjustmentFactor: number = 0;
|
||||
vAdjustmentFactor: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.diagramModel = new DiagramModel();
|
||||
this.nodeFactories = {};
|
||||
this.linkFactories = {};
|
||||
this.portFactories = {};
|
||||
this.labelFactories = {};
|
||||
this.canvas = null;
|
||||
this.paintableWidgets = null;
|
||||
this.linksThatHaveInitiallyRendered = {};
|
||||
this.smartRouting = false;
|
||||
|
||||
if (Toolkit.TESTING) {
|
||||
Toolkit.TESTING_UID = 0;
|
||||
|
||||
//pop it onto the window so our E2E helpers can find it
|
||||
if (window) {
|
||||
(window as any)["diagram_instance"] = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installDefaults() {
|
||||
super.installDefaults();
|
||||
this.registerElementFactory(new DefaultLabelFactory());
|
||||
this.registerElementFactory(new DefaultLinkFactory());
|
||||
this.registerElementFactory(new DefaultNodeFactory());
|
||||
this.registerElementFactory(new DefaultPortFactory());
|
||||
installDefaultFactories() {
|
||||
this.registerNodeFactory(new DefaultNodeFactory());
|
||||
this.registerLinkFactory(new DefaultLinkFactory());
|
||||
this.registerPortFactory(new DefaultPortFactory());
|
||||
this.registerLabelFactory(new DefaultLabelFactory());
|
||||
}
|
||||
|
||||
repaintCanvas() {
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.repaintCanvas) {
|
||||
listener.repaintCanvas();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearRepaintEntities() {
|
||||
this.paintableWidgets = null;
|
||||
}
|
||||
|
||||
enableRepaintEntities(entities: BaseModel<BaseEntity, BaseModelListener>[]) {
|
||||
this.paintableWidgets = {};
|
||||
entities.forEach(entity => {
|
||||
//if a node is requested to repaint, add all of its links
|
||||
if (entity instanceof NodeModel) {
|
||||
_.forEach(entity.getPorts(), port => {
|
||||
_.forEach(port.getLinks(), link => {
|
||||
this.paintableWidgets[link.getID()] = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (entity instanceof PointModel) {
|
||||
this.paintableWidgets[entity.getLink().getID()] = true;
|
||||
}
|
||||
|
||||
this.paintableWidgets[entity.getID()] = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a model is locked by running through
|
||||
* its parents to see if they are locked first
|
||||
*/
|
||||
isModelLocked(model: BaseEntity<BaseListener>) {
|
||||
//always check the diagram model
|
||||
if (this.diagramModel.isLocked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return model.isLocked();
|
||||
}
|
||||
|
||||
recalculatePortsVisually() {
|
||||
this.nodesRendered = false;
|
||||
this.linksThatHaveInitiallyRendered = {};
|
||||
}
|
||||
|
||||
canEntityRepaint(baseModel: BaseModel<BaseEntity, BaseModelListener>) {
|
||||
//no rules applied, allow repaint
|
||||
if (this.paintableWidgets === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.paintableWidgets[baseModel.getID()] !== undefined;
|
||||
}
|
||||
|
||||
setCanvas(canvas: Element | null) {
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
setDiagramModel(model: DiagramModel) {
|
||||
this.diagramModel = model;
|
||||
this.recalculatePortsVisually();
|
||||
}
|
||||
|
||||
getDiagramModel(): DiagramModel {
|
||||
return this.diagramModel;
|
||||
}
|
||||
|
||||
//!-------------- FACTORIES ------------
|
||||
|
||||
getNodeFactories(): { [s: string]: AbstractNodeFactory } {
|
||||
return this.nodeFactories;
|
||||
}
|
||||
|
||||
getLinkFactories(): { [s: string]: AbstractLinkFactory } {
|
||||
return this.linkFactories;
|
||||
}
|
||||
|
||||
getLabelFactories(): { [s: string]: AbstractLabelFactory } {
|
||||
return this.labelFactories;
|
||||
}
|
||||
|
||||
registerLabelFactory(factory: AbstractLabelFactory) {
|
||||
this.labelFactories[factory.getType()] = factory;
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.labelFactoriesUpdated) {
|
||||
listener.labelFactoriesUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerPortFactory(factory: AbstractPortFactory) {
|
||||
this.portFactories[factory.getType()] = factory;
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.portFactoriesUpdated) {
|
||||
listener.portFactoriesUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerNodeFactory(factory: AbstractNodeFactory) {
|
||||
this.nodeFactories[factory.getType()] = factory;
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.nodeFactoriesUpdated) {
|
||||
listener.nodeFactoriesUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerLinkFactory(factory: AbstractLinkFactory) {
|
||||
this.linkFactories[factory.getType()] = factory;
|
||||
this.iterateListeners(listener => {
|
||||
if (listener.linkFactoriesUpdated) {
|
||||
listener.linkFactoriesUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPortFactory(type: string): AbstractPortFactory {
|
||||
if (this.portFactories[type]) {
|
||||
return this.portFactories[type];
|
||||
}
|
||||
throw new Error(`cannot find factory for port of type: [${type}]`);
|
||||
}
|
||||
|
||||
getNodeFactory(type: string): AbstractNodeFactory {
|
||||
if (this.nodeFactories[type]) {
|
||||
return this.nodeFactories[type];
|
||||
}
|
||||
throw new Error(`cannot find factory for node of type: [${type}]`);
|
||||
}
|
||||
|
||||
getLinkFactory(type: string): AbstractLinkFactory {
|
||||
if (this.linkFactories[type]) {
|
||||
return this.linkFactories[type];
|
||||
}
|
||||
throw new Error(`cannot find factory for link of type: [${type}]`);
|
||||
}
|
||||
|
||||
getLabelFactory(type: string): AbstractLabelFactory {
|
||||
if (this.labelFactories[type]) {
|
||||
return this.labelFactories[type];
|
||||
}
|
||||
throw new Error(`cannot find factory for label of type: [${type}]`);
|
||||
}
|
||||
|
||||
getFactoryForNode(node: NodeModel): AbstractNodeFactory | null {
|
||||
return this.getNodeFactory(node.getType());
|
||||
}
|
||||
|
||||
getFactoryForLink(link: LinkModel): AbstractLinkFactory | null {
|
||||
return this.getLinkFactory(link.getType());
|
||||
}
|
||||
|
||||
getFactoryForLabel(label: LabelModel): AbstractLabelFactory | null {
|
||||
return this.getLabelFactory(label.getType());
|
||||
}
|
||||
|
||||
generateWidgetForLink(link: LinkModel): JSX.Element | null {
|
||||
var linkFactory = this.getFactoryForLink(link);
|
||||
if (!linkFactory) {
|
||||
throw new Error("Cannot find link factory for link: " + link.getType());
|
||||
}
|
||||
return linkFactory.generateReactWidget(this, link);
|
||||
}
|
||||
|
||||
generateWidgetForNode(node: NodeModel): JSX.Element | null {
|
||||
var nodeFactory = this.getFactoryForNode(node);
|
||||
if (!nodeFactory) {
|
||||
throw new Error("Cannot find widget factory for node: " + node.getType());
|
||||
}
|
||||
return nodeFactory.generateReactWidget(this, node);
|
||||
}
|
||||
|
||||
getRelativeMousePoint(event): { x: number; y: number } {
|
||||
var point = this.getRelativePoint(event.clientX, event.clientY);
|
||||
return {
|
||||
x: (point.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
|
||||
y: (point.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
|
||||
};
|
||||
}
|
||||
|
||||
getRelativePoint(x, y) {
|
||||
var canvasRect = this.canvas.getBoundingClientRect();
|
||||
return { x: x - canvasRect.left, y: y - canvasRect.top };
|
||||
}
|
||||
|
||||
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) {
|
||||
var sourceElement = this.getNodePortElement(port);
|
||||
var sourceRect = sourceElement.getBoundingClientRect();
|
||||
|
||||
var rel = this.getRelativePoint(sourceRect.left, sourceRect.top);
|
||||
|
||||
return {
|
||||
x:
|
||||
sourceElement.offsetWidth / 2 +
|
||||
(rel.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0),
|
||||
y:
|
||||
sourceElement.offsetHeight / 2 +
|
||||
(rel.y - this.diagramModel.getOffsetY()) / (this.diagramModel.getZoomLevel() / 100.0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rectangular coordinates of the port passed in.
|
||||
*/
|
||||
getPortCoords(
|
||||
port: PortModel
|
||||
): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const sourceElement = this.getNodePortElement(port);
|
||||
const sourceRect = sourceElement.getBoundingClientRect();
|
||||
const canvasRect = this.canvas.getBoundingClientRect() as ClientRect;
|
||||
|
||||
return {
|
||||
x:
|
||||
(sourceRect.x - this.diagramModel.getOffsetX()) / (this.diagramModel.getZoomLevel() / 100.0) -
|
||||
canvasRect.left,
|
||||
y:
|
||||
(sourceRect.y - 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 {
|
||||
@@ -32,10 +371,213 @@ export class DiagramEngine extends CanvasEngine<DiagramModel> {
|
||||
}
|
||||
|
||||
isSmartRoutingEnabled() {
|
||||
return this.smartRouting;
|
||||
return !!this.smartRouting;
|
||||
}
|
||||
|
||||
setSmartRoutingStatus(status: boolean) {
|
||||
this.smartRouting = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the canvas in the following format:
|
||||
*
|
||||
* +-----------------+
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* | 0 0 0 0 0 0 0 0 |
|
||||
* +-----------------+
|
||||
*
|
||||
* In which all walkable points are marked by zeros.
|
||||
* It uses @link{#ROUTING_SCALING_FACTOR} to reduce the matrix dimensions and improve performance.
|
||||
*/
|
||||
getCanvasMatrix(): number[][] {
|
||||
if (this.canvasMatrix.length === 0) {
|
||||
this.calculateCanvasMatrix();
|
||||
}
|
||||
|
||||
return this.canvasMatrix;
|
||||
}
|
||||
calculateCanvasMatrix() {
|
||||
const {
|
||||
width: canvasWidth,
|
||||
hAdjustmentFactor,
|
||||
height: canvasHeight,
|
||||
vAdjustmentFactor
|
||||
} = this.calculateMatrixDimensions();
|
||||
|
||||
this.hAdjustmentFactor = hAdjustmentFactor;
|
||||
this.vAdjustmentFactor = vAdjustmentFactor;
|
||||
|
||||
const matrixWidth = Math.ceil(canvasWidth / ROUTING_SCALING_FACTOR);
|
||||
const matrixHeight = Math.ceil(canvasHeight / ROUTING_SCALING_FACTOR);
|
||||
|
||||
this.canvasMatrix = _.range(0, matrixHeight).map(() => {
|
||||
return new Array(matrixWidth).fill(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the canvas in the following format:
|
||||
*
|
||||
* +-----------------+
|
||||
* | 0 0 1 1 0 0 0 0 |
|
||||
* | 0 0 1 1 0 0 1 1 |
|
||||
* | 0 0 0 0 0 0 1 1 |
|
||||
* | 1 1 0 0 0 0 0 0 |
|
||||
* | 1 1 0 0 0 0 0 0 |
|
||||
* +-----------------+
|
||||
*
|
||||
* In which all points blocked by a node (and its ports) are
|
||||
* marked as 1; points were there is nothing (ie, free) receive 0.
|
||||
*/
|
||||
getRoutingMatrix(): number[][] {
|
||||
if (this.routingMatrix.length === 0) {
|
||||
this.calculateRoutingMatrix();
|
||||
}
|
||||
|
||||
return this.routingMatrix;
|
||||
}
|
||||
calculateRoutingMatrix(): void {
|
||||
const matrix = _.cloneDeep(this.getCanvasMatrix());
|
||||
|
||||
// nodes need to be marked as blocked points
|
||||
this.markNodes(matrix);
|
||||
// same thing for ports
|
||||
this.markPorts(matrix);
|
||||
|
||||
this.routingMatrix = matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* The routing matrix does not have negative indexes, but elements could be negatively positioned.
|
||||
* We use the functions below to translate back and forth between these coordinates, relying on the
|
||||
* calculated values of hAdjustmentFactor and vAdjustmentFactor.
|
||||
*/
|
||||
translateRoutingX(x: number, reverse: boolean = false) {
|
||||
return x + this.hAdjustmentFactor * (reverse ? -1 : 1);
|
||||
}
|
||||
translateRoutingY(y: number, reverse: boolean = false) {
|
||||
return y + this.vAdjustmentFactor * (reverse ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Despite being a long method, we simply iterate over all three collections (nodes, ports and points)
|
||||
* to find the highest X and Y dimensions, so we can build the matrix large enough to contain all elements.
|
||||
*/
|
||||
calculateMatrixDimensions = (): {
|
||||
width: number;
|
||||
hAdjustmentFactor: number;
|
||||
height: number;
|
||||
vAdjustmentFactor: number;
|
||||
} => {
|
||||
const allNodesCoords = _.values(this.diagramModel.nodes).map(item => ({
|
||||
x: item.x,
|
||||
width: item.width,
|
||||
y: item.y,
|
||||
height: item.height
|
||||
}));
|
||||
|
||||
const allLinks = _.values(this.diagramModel.links);
|
||||
const allPortsCoords = _.flatMap(allLinks.map(link => [link.sourcePort, link.targetPort]))
|
||||
.filter(port => port !== null)
|
||||
.map(item => ({
|
||||
x: item.x,
|
||||
width: item.width,
|
||||
y: item.y,
|
||||
height: item.height
|
||||
}));
|
||||
const allPointsCoords = _.flatMap(allLinks.map(link => link.points)).map(item => ({
|
||||
// points don't have width/height, so let's just use 0
|
||||
x: item.x,
|
||||
width: 0,
|
||||
y: item.y,
|
||||
height: 0
|
||||
}));
|
||||
|
||||
const canvas = this.canvas as HTMLDivElement;
|
||||
const minX =
|
||||
Math.floor(
|
||||
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.x).x, 0) /
|
||||
ROUTING_SCALING_FACTOR
|
||||
) * ROUTING_SCALING_FACTOR;
|
||||
const maxXElement = _.maxBy(
|
||||
_.concat(allNodesCoords, allPortsCoords, allPointsCoords),
|
||||
item => item.x + item.width
|
||||
);
|
||||
const maxX = Math.max(maxXElement.x + maxXElement.width, canvas.offsetWidth);
|
||||
|
||||
const minY =
|
||||
Math.floor(
|
||||
Math.min(_.minBy(_.concat(allNodesCoords, allPortsCoords, allPointsCoords), item => item.y).y, 0) /
|
||||
ROUTING_SCALING_FACTOR
|
||||
) * ROUTING_SCALING_FACTOR;
|
||||
const maxYElement = _.maxBy(
|
||||
_.concat(allNodesCoords, allPortsCoords, allPointsCoords),
|
||||
item => item.y + item.height
|
||||
);
|
||||
const maxY = Math.max(maxYElement.y + maxYElement.height, canvas.offsetHeight);
|
||||
|
||||
return {
|
||||
width: Math.ceil(Math.abs(minX) + maxX),
|
||||
hAdjustmentFactor: Math.abs(minX) / ROUTING_SCALING_FACTOR + 1,
|
||||
height: Math.ceil(Math.abs(minY) + maxY),
|
||||
vAdjustmentFactor: Math.abs(minY) / ROUTING_SCALING_FACTOR + 1
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates (by reference) where nodes will be drawn on the matrix passed in.
|
||||
*/
|
||||
markNodes = (matrix: number[][]): void => {
|
||||
_.values(this.diagramModel.nodes).forEach(node => {
|
||||
const startX = Math.floor(node.x / ROUTING_SCALING_FACTOR);
|
||||
const endX = Math.ceil((node.x + node.width) / ROUTING_SCALING_FACTOR);
|
||||
const startY = Math.floor(node.y / ROUTING_SCALING_FACTOR);
|
||||
const endY = Math.ceil((node.y + node.height) / ROUTING_SCALING_FACTOR);
|
||||
|
||||
for (let x = startX - 1; x <= endX + 1; x++) {
|
||||
for (let y = startY - 1; y < endY + 1; y++) {
|
||||
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates (by reference) where ports will be drawn on the matrix passed in.
|
||||
*/
|
||||
markPorts = (matrix: number[][]): void => {
|
||||
const allElements = _.flatMap(
|
||||
_.values(this.diagramModel.links).map(link => [].concat(link.sourcePort, link.targetPort))
|
||||
);
|
||||
allElements.filter(port => port !== null).forEach(port => {
|
||||
const startX = Math.floor(port.x / ROUTING_SCALING_FACTOR);
|
||||
const endX = Math.ceil((port.x + port.width) / ROUTING_SCALING_FACTOR);
|
||||
const startY = Math.floor(port.y / ROUTING_SCALING_FACTOR);
|
||||
const endY = Math.ceil((port.y + port.height) / ROUTING_SCALING_FACTOR);
|
||||
|
||||
for (let x = startX - 1; x <= endX + 1; x++) {
|
||||
for (let y = startY - 1; y < endY + 1; y++) {
|
||||
this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
markMatrixPoint = (matrix: number[][], x: number, y: number) => {
|
||||
if (matrix[y] !== undefined && matrix[y][x] !== undefined) {
|
||||
matrix[y][x] = 1;
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,29 @@ import closest = require("closest");
|
||||
import { PointModel } from "./models/PointModel";
|
||||
import { ROUTING_SCALING_FACTOR } from "./routing/PathFinding";
|
||||
import * as Path from "paths-js/path";
|
||||
import { Toolkit as TK } from "@projectstorm/react-canvas";
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class Toolkit extends TK {
|
||||
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
|
||||
*
|
||||
@@ -27,8 +45,18 @@ export class Toolkit extends TK {
|
||||
|
||||
public static generateCurvePath(firstPoint: PointModel, lastPoint: PointModel, curvy: number = 0): string {
|
||||
var isHorizontal = Math.abs(firstPoint.x - lastPoint.x) > Math.abs(firstPoint.y - lastPoint.y);
|
||||
var curvyX = isHorizontal ? curvy : 0;
|
||||
var curvyY = isHorizontal ? 0 : curvy;
|
||||
|
||||
var xOrY = isHorizontal ? "x" : "y";
|
||||
|
||||
// make sure that smoothening works
|
||||
// without disrupting the line direction
|
||||
let curvyness = curvy;
|
||||
if (firstPoint[xOrY] > firstPoint[xOrY]) {
|
||||
curvyness = -curvy;
|
||||
}
|
||||
|
||||
var curvyX = isHorizontal ? curvyness : 0;
|
||||
var curvyY = isHorizontal ? 0 : curvyness;
|
||||
|
||||
return `M${firstPoint.x},${firstPoint.y} C ${firstPoint.x + curvyX},${firstPoint.y + curvyY}
|
||||
${lastPoint.x - curvyX},${lastPoint.y - curvyY} ${lastPoint.x},${lastPoint.y}`;
|
||||
|
||||
11
src/actions/BaseAction.ts
Normal file
11
src/actions/BaseAction.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export class BaseAction {
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
ms: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number) {
|
||||
this.mouseX = mouseX;
|
||||
this.mouseY = mouseY;
|
||||
this.ms = new Date().getTime();
|
||||
}
|
||||
}
|
||||
13
src/actions/MoveCanvasAction.ts
Normal file
13
src/actions/MoveCanvasAction.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BaseAction } from "./BaseAction";
|
||||
import { DiagramModel } from "../models/DiagramModel";
|
||||
|
||||
export class MoveCanvasAction extends BaseAction {
|
||||
initialOffsetX: number;
|
||||
initialOffsetY: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, diagramModel: DiagramModel) {
|
||||
super(mouseX, mouseY);
|
||||
this.initialOffsetX = diagramModel.getOffsetX();
|
||||
this.initialOffsetY = diagramModel.getOffsetY();
|
||||
}
|
||||
}
|
||||
30
src/actions/MoveItemsAction.ts
Normal file
30
src/actions/MoveItemsAction.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BaseAction } from "./BaseAction";
|
||||
import { SelectionModel } from "../models/SelectionModel";
|
||||
import { PointModel } from "../models/PointModel";
|
||||
import { NodeModel } from "../models/NodeModel";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
|
||||
export class MoveItemsAction extends BaseAction {
|
||||
selectionModels: SelectionModel[];
|
||||
moved: boolean;
|
||||
|
||||
constructor(mouseX: number, mouseY: number, diagramEngine: DiagramEngine) {
|
||||
super(mouseX, mouseY);
|
||||
this.moved = false;
|
||||
diagramEngine.enableRepaintEntities(diagramEngine.getDiagramModel().getSelectedItems());
|
||||
var selectedItems = diagramEngine.getDiagramModel().getSelectedItems();
|
||||
|
||||
//dont allow items which are locked to move
|
||||
selectedItems = selectedItems.filter(item => {
|
||||
return !diagramEngine.isModelLocked(item);
|
||||
});
|
||||
|
||||
this.selectionModels = selectedItems.map((item: PointModel | NodeModel) => {
|
||||
return {
|
||||
model: item,
|
||||
initialX: item.x,
|
||||
initialY: item.y
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
36
src/actions/SelectingAction.ts
Normal file
36
src/actions/SelectingAction.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { BaseAction } from "./BaseAction";
|
||||
import { DiagramModel } from "../models/DiagramModel";
|
||||
|
||||
export class SelectingAction extends BaseAction {
|
||||
mouseX2: number;
|
||||
mouseY2: number;
|
||||
|
||||
constructor(mouseX: number, mouseY: number) {
|
||||
super(mouseX, mouseY);
|
||||
this.mouseX2 = mouseX;
|
||||
this.mouseY2 = mouseY;
|
||||
}
|
||||
|
||||
getBoxDimensions() {
|
||||
return {
|
||||
left: this.mouseX2 > this.mouseX ? this.mouseX : this.mouseX2,
|
||||
top: this.mouseY2 > this.mouseY ? this.mouseY : this.mouseY2,
|
||||
width: Math.abs(this.mouseX2 - this.mouseX),
|
||||
height: Math.abs(this.mouseY2 - this.mouseY),
|
||||
right: this.mouseX2 < this.mouseX ? this.mouseX : this.mouseX2,
|
||||
bottom: this.mouseY2 < this.mouseY ? this.mouseY : this.mouseY2
|
||||
};
|
||||
}
|
||||
|
||||
containsElement(x: number, y: number, diagramModel: DiagramModel): boolean {
|
||||
var z = diagramModel.getZoomLevel() / 100.0;
|
||||
let dimensions = this.getBoxDimensions();
|
||||
|
||||
return (
|
||||
x * z + diagramModel.getOffsetX() > dimensions.left &&
|
||||
x * z + diagramModel.getOffsetX() < dimensions.right &&
|
||||
y * z + diagramModel.getOffsetY() > dimensions.top &&
|
||||
y * z + diagramModel.getOffsetY() < dimensions.bottom
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { AbstractLabelFactory } from "../../factories/AbstractLabelFactory";
|
||||
import { DefaultLabelModel } from "../models/DefaultLabelModel";
|
||||
import { DefaultLabelWidget } from "../widgets/DefaultLabelWidget";
|
||||
import { AbstractElementFactory } from "@projectstorm/react-canvas";
|
||||
|
||||
export class DefaultLabelFactory extends AbstractElementFactory<DefaultLabelModel> {
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DefaultLabelFactory extends AbstractLabelFactory<DefaultLabelModel> {
|
||||
constructor() {
|
||||
super("default");
|
||||
}
|
||||
|
||||
generateWidget(engine: DiagramEngine, model: DefaultLabelModel): JSX.Element {
|
||||
return <DefaultLabelWidget model={model} />;
|
||||
generateReactWidget(diagramEngine: DiagramEngine, label: DefaultLabelModel): JSX.Element {
|
||||
return <DefaultLabelWidget model={label} />;
|
||||
}
|
||||
|
||||
generateModel(): DefaultLabelModel {
|
||||
getNewInstance(initialConfig?: any): DefaultLabelModel {
|
||||
return new DefaultLabelModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import * as React from "react";
|
||||
import { DefaultLinkWidget } from "../widgets/DefaultLinkWidget";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { AbstractElementFactory } from "@projectstorm/react-canvas";
|
||||
import { AbstractLinkFactory } from "../../factories/AbstractLinkFactory";
|
||||
import { DefaultLinkModel } from "../models/DefaultLinkModel";
|
||||
|
||||
export class DefaultLinkFactory extends AbstractElementFactory<DefaultLinkModel> {
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DefaultLinkFactory extends AbstractLinkFactory<DefaultLinkModel> {
|
||||
constructor() {
|
||||
super("default");
|
||||
}
|
||||
|
||||
generateWidget(engine: DiagramEngine, model: DefaultLinkModel): JSX.Element {
|
||||
generateReactWidget(diagramEngine: DiagramEngine, link: DefaultLinkModel): JSX.Element {
|
||||
return React.createElement(DefaultLinkWidget, {
|
||||
link: model,
|
||||
diagramEngine: engine
|
||||
link: link,
|
||||
diagramEngine: diagramEngine
|
||||
});
|
||||
}
|
||||
|
||||
generateModel(): DefaultLinkModel {
|
||||
getNewInstance(initialConfig?: any): DefaultLinkModel {
|
||||
return new DefaultLinkModel();
|
||||
}
|
||||
|
||||
@@ -24,8 +27,8 @@ export class DefaultLinkFactory extends AbstractElementFactory<DefaultLinkModel>
|
||||
return (
|
||||
<path
|
||||
className={selected ? widget.bem("--path-selected") : ""}
|
||||
strokeWidth={model.getWidth()}
|
||||
stroke={model.getColor()}
|
||||
strokeWidth={model.width}
|
||||
stroke={model.color}
|
||||
d={path}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,21 +2,23 @@ import { DefaultNodeModel } from "../models/DefaultNodeModel";
|
||||
import * as React from "react";
|
||||
import { DefaultNodeWidget } from "../widgets/DefaultNodeWidget";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { AbstractElementFactory } from "@projectstorm/react-canvas";
|
||||
|
||||
export class DefaultNodeFactory extends AbstractElementFactory<DefaultNodeModel> {
|
||||
import { AbstractNodeFactory } from "../../factories/AbstractNodeFactory";
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DefaultNodeFactory extends AbstractNodeFactory<DefaultNodeModel> {
|
||||
constructor() {
|
||||
super("default");
|
||||
}
|
||||
|
||||
generateWidget(diagramEngine: DiagramEngine, model: DefaultNodeModel): JSX.Element {
|
||||
generateReactWidget(diagramEngine: DiagramEngine, node: DefaultNodeModel): JSX.Element {
|
||||
return React.createElement(DefaultNodeWidget, {
|
||||
node: model,
|
||||
node: node,
|
||||
diagramEngine: diagramEngine
|
||||
});
|
||||
}
|
||||
|
||||
generateModel(): DefaultNodeModel {
|
||||
getNewInstance(initialConfig?: any): DefaultNodeModel {
|
||||
return new DefaultNodeModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { DefaultPortModel } from "../models/DefaultPortModel";
|
||||
import { AbstractElementFactory } from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "storm-react-diagrams";
|
||||
import { AbstractPortFactory } from "../../factories/AbstractPortFactory";
|
||||
|
||||
export class DefaultPortFactory extends AbstractElementFactory<DefaultPortModel> {
|
||||
export class DefaultPortFactory extends AbstractPortFactory<DefaultPortModel> {
|
||||
constructor() {
|
||||
super("default");
|
||||
}
|
||||
|
||||
generateWidget(engine: DiagramEngine, model: DefaultPortModel): JSX.Element {
|
||||
return null;
|
||||
}
|
||||
|
||||
generateModel(): DefaultPortModel {
|
||||
getNewInstance(initialConfig?: any): DefaultPortModel {
|
||||
return new DefaultPortModel(true, "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LabelModel } from "../../models/LabelModel";
|
||||
import * as _ from "lodash";
|
||||
import { DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
|
||||
export class DefaultLabelModel extends LabelModel {
|
||||
protected label: string;
|
||||
label: string;
|
||||
|
||||
constructor() {
|
||||
super("default");
|
||||
@@ -14,9 +14,9 @@ export class DefaultLabelModel extends LabelModel {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.label = event.data.label;
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.label = ob.label;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
import { LinkModel, LinkModelListener } from "../../models/LinkModel";
|
||||
import { BaseEvent } from "../../BaseEntity";
|
||||
import * as _ from "lodash";
|
||||
import { PointModel } from "../../models/PointModel";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { DefaultLabelModel } from "./DefaultLabelModel";
|
||||
import { LabelModel } from "../../models/LabelModel";
|
||||
import { BaseEvent, DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
|
||||
export interface DefaultLinkModelListener extends LinkModelListener {
|
||||
colorChanged?(event: BaseEvent<DefaultLinkModel> & { color: null | string }): void;
|
||||
@@ -11,9 +16,9 @@ export interface DefaultLinkModelListener extends LinkModelListener {
|
||||
}
|
||||
|
||||
export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
|
||||
protected width: number;
|
||||
protected color: string;
|
||||
protected curvyness: number;
|
||||
width: number;
|
||||
color: string;
|
||||
curvyness: number;
|
||||
|
||||
constructor(type: string = "default") {
|
||||
super(type);
|
||||
@@ -30,11 +35,11 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
|
||||
});
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.color = event.data.color;
|
||||
this.width = event.data.width;
|
||||
this.curvyness = event.data.curvyness;
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.color = ob.color;
|
||||
this.width = ob.width;
|
||||
this.curvyness = ob.curvyness;
|
||||
}
|
||||
|
||||
addLabel(label: LabelModel | string) {
|
||||
@@ -48,7 +53,7 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
|
||||
|
||||
setWidth(width: number) {
|
||||
this.width = width;
|
||||
this.iterateListeners("width changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
|
||||
this.iterateListeners((listener: DefaultLinkModelListener, event: BaseEvent) => {
|
||||
if (listener.widthChanged) {
|
||||
listener.widthChanged({ ...event, width: width });
|
||||
}
|
||||
@@ -57,22 +62,10 @@ export class DefaultLinkModel extends LinkModel<DefaultLinkModelListener> {
|
||||
|
||||
setColor(color: string) {
|
||||
this.color = color;
|
||||
this.iterateListeners("color changed", (listener: DefaultLinkModelListener, event: BaseEvent) => {
|
||||
this.iterateListeners((listener: DefaultLinkModelListener, event: BaseEvent) => {
|
||||
if (listener.colorChanged) {
|
||||
listener.colorChanged({ ...event, color: color });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
return this.color;
|
||||
}
|
||||
|
||||
getCurvyness() {
|
||||
return this.curvyness;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import * as _ from "lodash";
|
||||
import { DefaultPortModel } from "./DefaultPortModel";
|
||||
import { NodeModel } from "../../models/NodeModel";
|
||||
import { Toolkit } from "../../Toolkit";
|
||||
import { DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
import * as _ from "lodash";
|
||||
|
||||
export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
|
||||
protected name: string;
|
||||
protected color: string;
|
||||
import { NodeModel, NodeModelListener } from "../../models/NodeModel";
|
||||
import { Toolkit } from "../../Toolkit";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DefaultNodeModel extends NodeModel<NodeModelListener> {
|
||||
name: string;
|
||||
color: string;
|
||||
ports: { [s: string]: DefaultPortModel };
|
||||
|
||||
constructor(name: string = "Untitled", color: string = "rgb(0,192,255)") {
|
||||
super("default");
|
||||
@@ -22,10 +27,10 @@ export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
|
||||
return this.addPort(new DefaultPortModel(false, Toolkit.UID(), label));
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.name = event.data.name;
|
||||
this.color = event.data.color;
|
||||
deSerialize(object, engine: DiagramEngine) {
|
||||
super.deSerialize(object, engine);
|
||||
this.name = object.name;
|
||||
this.color = object.color;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
@@ -36,13 +41,13 @@ export class DefaultNodeModel extends NodeModel<DefaultPortModel> {
|
||||
}
|
||||
|
||||
getInPorts(): DefaultPortModel[] {
|
||||
return _.filter(this.getPorts(), portModel => {
|
||||
return _.filter(this.ports, portModel => {
|
||||
return portModel.in;
|
||||
});
|
||||
}
|
||||
|
||||
getOutPorts(): DefaultPortModel[] {
|
||||
return _.filter(this.getPorts(), portModel => {
|
||||
return _.filter(this.ports, portModel => {
|
||||
return !portModel.in;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import * as _ from "lodash";
|
||||
import { PortModel } from "../../models/PortModel";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { DefaultLinkModel } from "./DefaultLinkModel";
|
||||
import { LinkModel } from "../../models/LinkModel";
|
||||
import { DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
|
||||
export class DefaultPortModel extends PortModel {
|
||||
in: boolean;
|
||||
label: string;
|
||||
links: { [id: string]: DefaultLinkModel };
|
||||
|
||||
constructor(isInput: boolean, name: string, label: string = null) {
|
||||
super(name, "default");
|
||||
constructor(isInput: boolean, name: string, label: string = null, id?: string) {
|
||||
super(name, "default", id);
|
||||
this.in = isInput;
|
||||
this.label = label || name;
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.in = event.data.in;
|
||||
this.label = event.data.label;
|
||||
deSerialize(object, engine: DiagramEngine) {
|
||||
super.deSerialize(object, engine);
|
||||
this.in = object.in;
|
||||
this.label = object.label;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { DefaultLabelModel } from "../models/DefaultLabelModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
|
||||
|
||||
export interface DefaultLabelWidgetProps extends BaseWidgetProps {
|
||||
model: DefaultLabelModel;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DefaultLinkModel } from "../models/DefaultLinkModel";
|
||||
import PathFinding from "../../routing/PathFinding";
|
||||
import * as _ from "lodash";
|
||||
import { LabelModel } from "../../models/LabelModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
|
||||
|
||||
export interface DefaultLinkProps extends BaseWidgetProps {
|
||||
color?: string;
|
||||
@@ -70,7 +70,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
}
|
||||
}
|
||||
|
||||
addPointToLink(event: MouseEvent, index: number) {
|
||||
addPointToLink = (event: MouseEvent, index: number): void => {
|
||||
if (
|
||||
!event.shiftKey &&
|
||||
!this.props.diagramEngine.isModelLocked(this.props.link) &&
|
||||
@@ -82,14 +82,14 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
this.props.link.addPoint(point, index);
|
||||
this.props.pointAdded(point, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generatePoint(pointIndex: number): JSX.Element {
|
||||
let x = this.props.link.points[pointIndex].getPoint().x;
|
||||
let y = this.props.link.points[pointIndex].getPoint().y;
|
||||
let x = this.props.link.points[pointIndex].x;
|
||||
let y = this.props.link.points[pointIndex].y;
|
||||
|
||||
return (
|
||||
<g key={"point-" + this.props.link.points[pointIndex].getID()}>
|
||||
<g key={"point-" + this.props.link.points[pointIndex].id}>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
@@ -107,8 +107,8 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
onMouseEnter={() => {
|
||||
this.setState({ selected: true });
|
||||
}}
|
||||
data-id={this.props.link.points[pointIndex].getID()}
|
||||
data-linkid={this.props.link.getID()}
|
||||
data-id={this.props.link.points[pointIndex].id}
|
||||
data-linkid={this.props.link.id}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={15}
|
||||
@@ -123,15 +123,15 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
const canvas = this.props.diagramEngine.canvas as HTMLElement;
|
||||
return (
|
||||
<foreignObject
|
||||
key={label.getID()}
|
||||
key={label.id}
|
||||
className={this.bem("__label")}
|
||||
width={canvas.offsetWidth}
|
||||
height={canvas.offsetHeight}
|
||||
>
|
||||
<div ref={ref => (this.refLabels[label.getID()] = ref)}>
|
||||
<div ref={ref => (this.refLabels[label.id] = ref)}>
|
||||
{this.props.diagramEngine
|
||||
.getFactoryForElement(label)
|
||||
.generateWidget(this.props.diagramEngine, label)}
|
||||
.getFactoryForLabel(label)
|
||||
.generateReactWidget(this.props.diagramEngine, label)}
|
||||
</div>
|
||||
</foreignObject>
|
||||
);
|
||||
@@ -141,7 +141,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
var props = this.props;
|
||||
|
||||
var Bottom = React.cloneElement(
|
||||
(props.diagramEngine.getFactoryForElement(this.props.link) as DefaultLinkFactory).generateLinkSegment(
|
||||
(props.diagramEngine.getFactoryForLink(this.props.link) as DefaultLinkFactory).generateLinkSegment(
|
||||
this.props.link,
|
||||
this,
|
||||
this.state.selected || this.props.link.isSelected(),
|
||||
@@ -267,10 +267,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
|
||||
if (this.isSmartRoutingApplicable()) {
|
||||
// first step: calculate a direct path between the points being linked
|
||||
const directPathCoords = this.pathFinding.calculateDirectPath(
|
||||
_.first(points).getPoint(),
|
||||
_.last(points).getPoint()
|
||||
);
|
||||
const directPathCoords = this.pathFinding.calculateDirectPath(_.first(points), _.last(points));
|
||||
|
||||
const routingMatrix = diagramEngine.getRoutingMatrix();
|
||||
// now we need to extract, from the routing matrix, the very first walkable points
|
||||
@@ -308,9 +305,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
// See @link{#isSmartRoutingApplicable()}.
|
||||
if (paths.length === 0) {
|
||||
if (points.length === 2) {
|
||||
var isHorizontal =
|
||||
Math.abs(points[0].getPoint().x - points[1].getPoint().x) >
|
||||
Math.abs(points[0].getPoint().x - points[1].getPoint().y);
|
||||
var isHorizontal = Math.abs(points[0].x - points[1].x) > Math.abs(points[0].y - points[1].y);
|
||||
var xOrY = isHorizontal ? "x" : "y";
|
||||
|
||||
//draw the smoothing
|
||||
@@ -323,16 +318,9 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
var pointLeft = points[0];
|
||||
var pointRight = points[1];
|
||||
|
||||
//some defensive programming to make sure the smoothing is
|
||||
//always in the right direction
|
||||
if (pointLeft[xOrY] > pointRight[xOrY]) {
|
||||
pointLeft = points[1];
|
||||
pointRight = points[0];
|
||||
}
|
||||
|
||||
paths.push(
|
||||
this.generateLink(
|
||||
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.getCurvyness()),
|
||||
Toolkit.generateCurvePath(pointLeft, pointRight, this.props.link.curvyness),
|
||||
{
|
||||
onMouseDown: event => {
|
||||
this.addPointToLink(event, 1);
|
||||
@@ -353,7 +341,7 @@ export class DefaultLinkWidget extends BaseWidget<DefaultLinkProps, DefaultLinkS
|
||||
this.generateLink(
|
||||
Toolkit.generateLinePath(points[j], points[j + 1]),
|
||||
{
|
||||
"data-linkid": this.props.link.getID(),
|
||||
"data-linkid": this.props.link.id,
|
||||
"data-point": j,
|
||||
onMouseDown: (event: MouseEvent) => {
|
||||
this.addPointToLink(event, j + 1);
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as _ from "lodash";
|
||||
import { DefaultNodeModel } from "../models/DefaultNodeModel";
|
||||
import { DefaultPortLabel } from "./DefaultPortLabelWidget";
|
||||
import { DiagramEngine } from "../../DiagramEngine";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
|
||||
|
||||
export interface DefaultNodeProps extends BaseWidgetProps {
|
||||
node: DefaultNodeModel;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { DefaultPortModel } from "../models/DefaultPortModel";
|
||||
import { PortWidget } from "../../widgets/PortWidget";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { BaseWidget, BaseWidgetProps } from "../../widgets/BaseWidget";
|
||||
|
||||
export interface DefaultPortLabelProps extends BaseWidgetProps {
|
||||
model: DefaultPortModel;
|
||||
|
||||
15
src/factories/AbstractFactory.ts
Normal file
15
src/factories/AbstractFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BaseModel } from "../models/BaseModel";
|
||||
|
||||
export abstract class AbstractFactory<T extends BaseModel> {
|
||||
type: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.type = name;
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
abstract getNewInstance(initialConfig?: any): T;
|
||||
}
|
||||
7
src/factories/AbstractLabelFactory.ts
Normal file
7
src/factories/AbstractLabelFactory.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LabelModel } from "../models/LabelModel";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { AbstractFactory } from "./AbstractFactory";
|
||||
|
||||
export abstract class AbstractLabelFactory<T extends LabelModel = LabelModel> extends AbstractFactory<T> {
|
||||
abstract generateReactWidget(diagramEngine: DiagramEngine, link: T): JSX.Element;
|
||||
}
|
||||
7
src/factories/AbstractLinkFactory.ts
Normal file
7
src/factories/AbstractLinkFactory.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LinkModel } from "../models/LinkModel";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { AbstractFactory } from "./AbstractFactory";
|
||||
|
||||
export abstract class AbstractLinkFactory<T extends LinkModel = LinkModel> extends AbstractFactory<T> {
|
||||
abstract generateReactWidget(diagramEngine: DiagramEngine, link: T): JSX.Element;
|
||||
}
|
||||
7
src/factories/AbstractNodeFactory.ts
Normal file
7
src/factories/AbstractNodeFactory.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NodeModel } from "../models/NodeModel";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { AbstractFactory } from "./AbstractFactory";
|
||||
|
||||
export abstract class AbstractNodeFactory<T extends NodeModel = NodeModel> extends AbstractFactory<T> {
|
||||
abstract generateReactWidget(diagramEngine: DiagramEngine, node: T): JSX.Element;
|
||||
}
|
||||
5
src/factories/AbstractPortFactory.ts
Normal file
5
src/factories/AbstractPortFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PortModel } from "../models/PortModel";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { AbstractFactory } from "./AbstractFactory";
|
||||
|
||||
export abstract class AbstractPortFactory<T extends PortModel = PortModel> extends AbstractFactory<T> {}
|
||||
20
src/main.ts
20
src/main.ts
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
export * from "./Toolkit";
|
||||
export * from "./BaseEntity";
|
||||
export * from "./DiagramEngine";
|
||||
|
||||
export * from "./defaults/models/DefaultNodeModel";
|
||||
@@ -20,8 +21,21 @@ export * from "./defaults/widgets/DefaultLabelWidget";
|
||||
export * from "./defaults/widgets/DefaultNodeWidget";
|
||||
export * from "./defaults/widgets/DefaultPortLabelWidget";
|
||||
|
||||
export * from "./factories/AbstractFactory";
|
||||
export * from "./factories/AbstractLabelFactory";
|
||||
export * from "./factories/AbstractLinkFactory";
|
||||
export * from "./factories/AbstractNodeFactory";
|
||||
export * from "./factories/AbstractPortFactory";
|
||||
|
||||
export * from "./routing/PathFinding";
|
||||
|
||||
export * from "./actions/BaseAction";
|
||||
export * from "./actions/MoveCanvasAction";
|
||||
export * from "./actions/MoveItemsAction";
|
||||
export * from "./actions/SelectingAction";
|
||||
|
||||
export * from "./models/SelectionModel";
|
||||
export * from "./models/BaseModel";
|
||||
export * from "./models/DiagramModel";
|
||||
export * from "./models/LinkModel";
|
||||
export * from "./models/NodeModel";
|
||||
@@ -29,5 +43,11 @@ export * from "./models/PointModel";
|
||||
export * from "./models/PortModel";
|
||||
export * from "./models/LabelModel";
|
||||
|
||||
export * from "./widgets/DiagramWidget";
|
||||
export * from "./widgets/LinkWidget";
|
||||
export * from "./widgets/NodeWidget";
|
||||
export * from "./widgets/PortWidget";
|
||||
export * from "./widgets/BaseWidget";
|
||||
|
||||
export * from "./widgets/layers/LinkLayerWidget";
|
||||
export * from "./widgets/layers/NodeLayerWidget";
|
||||
|
||||
86
src/models/BaseModel.ts
Normal file
86
src/models/BaseModel.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { BaseEntity, BaseListener } from "../BaseEntity";
|
||||
import * as _ from "lodash";
|
||||
import { BaseEvent } from "../BaseEntity";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { PointModel } from "../models/PointModel";
|
||||
|
||||
export interface BaseModelListener extends BaseListener {
|
||||
selectionChanged?(event: BaseEvent<BaseModel> & { isSelected: boolean }): void;
|
||||
|
||||
entityRemoved?(event: BaseEvent<BaseModel>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class BaseModel<
|
||||
X extends BaseEntity = BaseEntity,
|
||||
T extends BaseModelListener = BaseModelListener
|
||||
> extends BaseEntity<T> {
|
||||
type: string;
|
||||
selected: boolean;
|
||||
parent: X;
|
||||
|
||||
constructor(type?: string, id?: string) {
|
||||
super(id);
|
||||
this.type = type;
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
public getParent(): X {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public setParent(parent: X) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public getSelectedEntities(): Array<BaseModel<any, T> | PointModel> {
|
||||
if (this.isSelected()) {
|
||||
return [this];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.type = ob.type;
|
||||
this.selected = ob.selected;
|
||||
}
|
||||
|
||||
public serialize() {
|
||||
return _.merge(super.serialize(), {
|
||||
type: this.type,
|
||||
selected: this.selected
|
||||
});
|
||||
}
|
||||
|
||||
public getType(): string {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public getID(): string {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public isSelected(): boolean {
|
||||
return this.selected;
|
||||
}
|
||||
|
||||
public setSelected(selected: boolean = true) {
|
||||
this.selected = selected;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.selectionChanged) {
|
||||
listener.selectionChanged({ ...event, isSelected: selected });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.entityRemoved) {
|
||||
listener.entityRemoved(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,240 @@
|
||||
import { BaseListener, BaseEntity, BaseEvent, BaseEntityType } from "../BaseEntity";
|
||||
import * as _ from "lodash";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { LinkModel } from "./LinkModel";
|
||||
import { NodeModel } from "./NodeModel";
|
||||
import { BaseModel, BaseEvent, CanvasModel, CanvasModelListener, CanvasLayerModel } from "@projectstorm/react-canvas";
|
||||
|
||||
export interface DiagramListener extends CanvasModelListener {
|
||||
import { PortModel } from "./PortModel";
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { PointModel } from "./PointModel";
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*
|
||||
*/
|
||||
export interface DiagramListener extends BaseListener {
|
||||
nodesUpdated?(event: BaseEvent & { node: NodeModel; isCreated: boolean }): void;
|
||||
|
||||
linksUpdated?(event: BaseEvent & { link: LinkModel; isCreated: boolean }): void;
|
||||
|
||||
offsetUpdated?(event: BaseEvent<DiagramModel> & { offsetX: number; offsetY: number }): void;
|
||||
|
||||
zoomUpdated?(event: BaseEvent<DiagramModel> & { zoom: number }): void;
|
||||
|
||||
gridUpdated?(event: BaseEvent<DiagramModel> & { size: number }): void;
|
||||
}
|
||||
|
||||
export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
linksLayer: CanvasLayerModel<LinkModel>;
|
||||
nodesLayer: CanvasLayerModel<NodeModel>;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class DiagramModel extends BaseEntity<DiagramListener> {
|
||||
//models
|
||||
links: { [s: string]: LinkModel };
|
||||
nodes: { [s: string]: NodeModel };
|
||||
|
||||
//control variables
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
zoom: number;
|
||||
rendered: boolean;
|
||||
gridSize: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.linksLayer = new CanvasLayerModel();
|
||||
this.nodesLayer = new CanvasLayerModel();
|
||||
this.links = {};
|
||||
this.nodes = {};
|
||||
|
||||
this.linksLayer.setSVG(true);
|
||||
this.linksLayer.setTransformable(true);
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
this.zoom = 100;
|
||||
this.rendered = false;
|
||||
this.gridSize = 0;
|
||||
}
|
||||
|
||||
this.nodesLayer.setSVG(false);
|
||||
this.nodesLayer.setTransformable(true);
|
||||
setGridSize(size: number = 0) {
|
||||
this.gridSize = size;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.gridUpdated) {
|
||||
listener.gridUpdated({ ...event, size: size });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.addLayer(this.linksLayer);
|
||||
this.addLayer(this.nodesLayer);
|
||||
getGridPosition(pos) {
|
||||
if (this.gridSize === 0) {
|
||||
return pos;
|
||||
}
|
||||
return this.gridSize * Math.floor((pos + this.gridSize / 2) / this.gridSize);
|
||||
}
|
||||
|
||||
deSerializeDiagram(object: any, diagramEngine: DiagramEngine) {
|
||||
this.deSerialize(object, diagramEngine);
|
||||
|
||||
this.offsetX = object.offsetX;
|
||||
this.offsetY = object.offsetY;
|
||||
this.zoom = object.zoom;
|
||||
this.gridSize = object.gridSize;
|
||||
|
||||
// deserialize nodes
|
||||
_.forEach(object.nodes, (node: any) => {
|
||||
let nodeOb = diagramEngine.getNodeFactory(node.type).getNewInstance(node);
|
||||
nodeOb.setParent(this);
|
||||
nodeOb.deSerialize(node, diagramEngine);
|
||||
this.addNode(nodeOb);
|
||||
});
|
||||
|
||||
// deserialze links
|
||||
_.forEach(object.links, (link: any) => {
|
||||
let linkOb = diagramEngine.getLinkFactory(link.type).getNewInstance();
|
||||
linkOb.setParent(this);
|
||||
linkOb.deSerialize(link, diagramEngine);
|
||||
this.addLink(linkOb);
|
||||
});
|
||||
}
|
||||
|
||||
serializeDiagram() {
|
||||
return _.merge(this.serialize(), {
|
||||
offsetX: this.offsetX,
|
||||
offsetY: this.offsetY,
|
||||
zoom: this.zoom,
|
||||
gridSize: this.gridSize,
|
||||
links: _.map(this.links, link => {
|
||||
return link.serialize();
|
||||
}),
|
||||
nodes: _.map(this.nodes, node => {
|
||||
return node.serialize();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
clearSelection(ignore: BaseModel<BaseEntity, BaseModelListener> | null = null) {
|
||||
_.forEach(this.getSelectedItems(), element => {
|
||||
if (ignore && ignore.getID() === element.getID()) {
|
||||
return;
|
||||
}
|
||||
element.setSelected(false); //TODO dont fire the listener
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedItems(...filters: BaseEntityType[]): BaseModel<BaseEntity, BaseModelListener>[] {
|
||||
if (!Array.isArray(filters)) {
|
||||
filters = [filters];
|
||||
}
|
||||
var items = [];
|
||||
|
||||
// run through nodes
|
||||
items = items.concat(
|
||||
_.flatMap(this.nodes, node => {
|
||||
return node.getSelectedEntities();
|
||||
})
|
||||
);
|
||||
|
||||
// find all the links
|
||||
items = items.concat(
|
||||
_.flatMap(this.links, link => {
|
||||
return link.getSelectedEntities();
|
||||
})
|
||||
);
|
||||
|
||||
//find all points
|
||||
items = items.concat(
|
||||
_.flatMap(this.links, link => {
|
||||
return _.flatMap(link.points, point => {
|
||||
return point.getSelectedEntities();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
items = _.uniq(items);
|
||||
|
||||
if (filters.length > 0) {
|
||||
items = _.filter(_.uniq(items), (item: BaseModel<any>) => {
|
||||
if (_.includes(filters, "node") && item instanceof NodeModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, "link") && item instanceof LinkModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, "port") && item instanceof PortModel) {
|
||||
return true;
|
||||
}
|
||||
if (_.includes(filters, "point") && item instanceof PointModel) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
setZoomLevel(zoom: number) {
|
||||
this.zoom = zoom;
|
||||
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.zoomUpdated) {
|
||||
listener.zoomUpdated({ ...event, zoom: zoom });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOffset(offsetX: number, offsetY: number) {
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.offsetUpdated) {
|
||||
listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: offsetY });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOffsetX(offsetX: number) {
|
||||
this.offsetX = offsetX;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.offsetUpdated) {
|
||||
listener.offsetUpdated({ ...event, offsetX: offsetX, offsetY: this.offsetY });
|
||||
}
|
||||
});
|
||||
}
|
||||
setOffsetY(offsetY: number) {
|
||||
this.offsetY = offsetY;
|
||||
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.offsetUpdated) {
|
||||
listener.offsetUpdated({ ...event, offsetX: this.offsetX, offsetY: this.offsetY });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOffsetY() {
|
||||
return this.offsetY;
|
||||
}
|
||||
|
||||
getOffsetX() {
|
||||
return this.offsetX;
|
||||
}
|
||||
|
||||
getZoomLevel() {
|
||||
return this.zoom;
|
||||
}
|
||||
|
||||
getNode(node: string | NodeModel): NodeModel | null {
|
||||
if (node instanceof NodeModel) {
|
||||
return node;
|
||||
}
|
||||
return this.nodesLayer.getEntity[node] || null;
|
||||
if (!this.nodes[node]) {
|
||||
return null;
|
||||
}
|
||||
return this.nodes[node];
|
||||
}
|
||||
|
||||
getLink(link: string | LinkModel): LinkModel | null {
|
||||
if (link instanceof LinkModel) {
|
||||
return link;
|
||||
}
|
||||
return this.linksLayer.getEntity(link) || null;
|
||||
if (!this.links[link]) {
|
||||
return null;
|
||||
}
|
||||
return this.links[link];
|
||||
}
|
||||
|
||||
addAll(...models: BaseModel[]): BaseModel[] {
|
||||
@@ -57,8 +249,13 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
}
|
||||
|
||||
addLink(link: LinkModel): LinkModel {
|
||||
this.linksLayer.addEntity(link);
|
||||
this.iterateListeners("link added", (listener, event) => {
|
||||
link.addListener({
|
||||
entityRemoved: () => {
|
||||
this.removeLink(link);
|
||||
}
|
||||
});
|
||||
this.links[link.getID()] = link;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.linksUpdated) {
|
||||
listener.linksUpdated({ ...event, link: link, isCreated: true });
|
||||
}
|
||||
@@ -67,8 +264,13 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
}
|
||||
|
||||
addNode(node: NodeModel): NodeModel {
|
||||
this.nodesLayer.addEntity(node);
|
||||
this.iterateListeners("node added", (listener, event) => {
|
||||
node.addListener({
|
||||
entityRemoved: () => {
|
||||
this.removeNode(node);
|
||||
}
|
||||
});
|
||||
this.nodes[node.getID()] = node;
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.nodesUpdated) {
|
||||
listener.nodesUpdated({ ...event, node: node, isCreated: true });
|
||||
}
|
||||
@@ -77,8 +279,9 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
}
|
||||
|
||||
removeLink(link: LinkModel | string) {
|
||||
this.linksLayer.removeEntity(link);
|
||||
this.iterateListeners("link removed", (listener, event) => {
|
||||
link = this.getLink(link);
|
||||
delete this.links[link.getID()];
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.linksUpdated) {
|
||||
listener.linksUpdated({ ...event, link: link as LinkModel, isCreated: false });
|
||||
}
|
||||
@@ -86,8 +289,9 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
}
|
||||
|
||||
removeNode(node: NodeModel | string) {
|
||||
this.nodesLayer.removeEntity(node);
|
||||
this.iterateListeners("node removed", (listener, event) => {
|
||||
node = this.getNode(node);
|
||||
delete this.nodes[node.getID()];
|
||||
this.iterateListeners((listener, event) => {
|
||||
if (listener.nodesUpdated) {
|
||||
listener.nodesUpdated({ ...event, node: node as NodeModel, isCreated: false });
|
||||
}
|
||||
@@ -95,10 +299,10 @@ export class DiagramModel extends CanvasModel<DiagramListener> {
|
||||
}
|
||||
|
||||
getLinks(): { [s: string]: LinkModel } {
|
||||
return this.linksLayer.getEntities();
|
||||
return this.links;
|
||||
}
|
||||
|
||||
getNodes(): { [s: string]: NodeModel } {
|
||||
return this.nodesLayer.getEntities();
|
||||
return this.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { BaseModel } from "./BaseModel";
|
||||
import { LinkModel } from "./LinkModel";
|
||||
import * as _ from "lodash";
|
||||
import { BaseModel, DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
|
||||
export class LabelModel extends BaseModel<LinkModel> {
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
|
||||
constructor(type?: string) {
|
||||
super(type);
|
||||
constructor(type?: string, id?: string) {
|
||||
super(type, id);
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.offsetX = event.data.offsetX;
|
||||
this.offsetY = event.data.offsetY;
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.offsetX = ob.offsetX;
|
||||
this.offsetY = ob.offsetY;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
||||
@@ -1,71 +1,97 @@
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { PortModel } from "./PortModel";
|
||||
import { PointModel } from "./PointModel";
|
||||
import * as _ from "lodash";
|
||||
import { BaseEvent } from "../BaseEntity";
|
||||
import { LabelModel } from "./LabelModel";
|
||||
import {
|
||||
BaseEvent,
|
||||
GraphModel,
|
||||
GraphModelOrdered,
|
||||
CanvasElementModel,
|
||||
CanvasElementModelListener,
|
||||
Rectangle,
|
||||
DeserializeEvent
|
||||
} from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { DiagramModel } from "./DiagramModel";
|
||||
|
||||
export interface LinkModelListener<T extends LinkModel = any> extends CanvasElementModelListener<T> {
|
||||
sourcePortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
|
||||
export interface LinkModelListener extends BaseModelListener {
|
||||
sourcePortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
|
||||
|
||||
targetPortChanged?(event: BaseEvent<T> & { port: null | PortModel }): void;
|
||||
targetPortChanged?(event: BaseEvent<LinkModel> & { port: null | PortModel }): void;
|
||||
}
|
||||
|
||||
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends CanvasElementModel<T> {
|
||||
protected sourcePort: PortModel | null;
|
||||
protected targetPort: PortModel | null;
|
||||
protected labels: GraphModel<LabelModel, LinkModel>;
|
||||
protected points: GraphModelOrdered<PointModel, LinkModel>;
|
||||
export class LinkModel<T extends LinkModelListener = LinkModelListener> extends BaseModel<DiagramModel, T> {
|
||||
sourcePort: PortModel | null;
|
||||
targetPort: PortModel | null;
|
||||
labels: LabelModel[];
|
||||
points: PointModel[];
|
||||
extras: any;
|
||||
|
||||
constructor(linkType: string = "default") {
|
||||
super(linkType);
|
||||
this.points = new GraphModelOrdered();
|
||||
this.labels = new GraphModel();
|
||||
this.points.setParentDelegate(this);
|
||||
this.labels.setParentDelegate(this);
|
||||
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 = {};
|
||||
this.sourcePort = null;
|
||||
this.targetPort = null;
|
||||
this.labels = [];
|
||||
}
|
||||
|
||||
setDimensions(dimensions: Rectangle) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
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 });
|
||||
p.deSerialize(point, engine);
|
||||
return p;
|
||||
});
|
||||
|
||||
getDimensions(): Rectangle {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
//deserialize labels
|
||||
_.forEach(ob.labels || [], (label: any) => {
|
||||
let labelOb = engine.getLabelFactory(label.type).getNewInstance();
|
||||
labelOb.deSerialize(label, engine);
|
||||
this.addLabel(labelOb);
|
||||
});
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.points.deSerialize(event.subset("points"));
|
||||
this.labels.deSerialize(event.subset("labels"));
|
||||
if (event.data.target) {
|
||||
this.setTargetPort(event.cache[event.data.targetPort] as PortModel);
|
||||
if (ob.target) {
|
||||
this.setTargetPort(
|
||||
this.getParent()
|
||||
.getNode(ob.target)
|
||||
.getPortFromID(ob.targetPort)
|
||||
);
|
||||
}
|
||||
|
||||
if (event.data.source) {
|
||||
this.setSourcePort(event.cache[event.data.sourcePort] as PortModel);
|
||||
if (ob.source) {
|
||||
this.setSourcePort(
|
||||
this.getParent()
|
||||
.getNode(ob.source)
|
||||
.getPortFromID(ob.sourcePort)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return _.merge(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: this.points.serialize(),
|
||||
labels: this.labels.serialize()
|
||||
source: this.sourcePort ? this.sourcePort.getParent().id : null,
|
||||
sourcePort: this.sourcePort ? this.sourcePort.id : null,
|
||||
target: this.targetPort ? this.targetPort.getParent().id : null,
|
||||
targetPort: this.targetPort ? this.targetPort.id : null,
|
||||
points: _.map(this.points, point => {
|
||||
return point.serialize();
|
||||
}),
|
||||
extras: this.extras,
|
||||
labels: _.map(this.labels, label => {
|
||||
return label.serialize();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
doClone(lookupTable = {}, clone) {
|
||||
clone.setPoints(
|
||||
_.map(this.getPoints(), (point: PointModel) => {
|
||||
return point.clone(lookupTable);
|
||||
})
|
||||
);
|
||||
if (this.sourcePort) {
|
||||
clone.setSourcePort(this.sourcePort.clone(lookupTable));
|
||||
}
|
||||
if (this.targetPort) {
|
||||
clone.setTargetPort(this.targetPort.clone(lookupTable));
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.sourcePort) {
|
||||
this.sourcePort.removeLink(this);
|
||||
@@ -73,19 +99,25 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
|
||||
if (this.targetPort) {
|
||||
this.targetPort.removeLink(this);
|
||||
}
|
||||
super.remove();
|
||||
}
|
||||
|
||||
isLastPoint(point: PointModel) {
|
||||
var index = this.getPointIndex(point);
|
||||
return index === this.points.count() - 1;
|
||||
return index === this.points.length - 1;
|
||||
}
|
||||
|
||||
getPointIndex(point: PointModel) {
|
||||
return _.values(this.points.getEntities()).indexOf(point);
|
||||
return this.points.indexOf(point);
|
||||
}
|
||||
|
||||
getPointModel(id: string): PointModel | null {
|
||||
return this.points.getEntities()[id];
|
||||
for (var i = 0; i < this.points.length; i++) {
|
||||
if (this.points[i].id === id) {
|
||||
return this.points[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getPortForPoint(point: PointModel): PortModel {
|
||||
@@ -109,11 +141,11 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
|
||||
}
|
||||
|
||||
getFirstPoint(): PointModel {
|
||||
return _.values(this.points.getEntities())[0];
|
||||
return this.points[0];
|
||||
}
|
||||
|
||||
getLastPoint(): PointModel {
|
||||
return _.values(this.points.getEntities())[this.points.count() - 1];
|
||||
return this.points[this.points.length - 1];
|
||||
}
|
||||
|
||||
setSourcePort(port: PortModel) {
|
||||
@@ -124,7 +156,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
|
||||
this.sourcePort.removeLink(this);
|
||||
}
|
||||
this.sourcePort = port;
|
||||
this.iterateListeners("source port changed", (listener: T, event) => {
|
||||
this.iterateListeners((listener: LinkModelListener, event) => {
|
||||
if (listener.sourcePortChanged) {
|
||||
listener.sourcePortChanged({ ...event, port: port });
|
||||
}
|
||||
@@ -147,7 +179,7 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
|
||||
this.targetPort.removeLink(this);
|
||||
}
|
||||
this.targetPort = port;
|
||||
this.iterateListeners("target port chnaged", (listener: T, event) => {
|
||||
this.iterateListeners((listener: LinkModelListener, event) => {
|
||||
if (listener.targetPortChanged) {
|
||||
listener.targetPortChanged({ ...event, port: port });
|
||||
}
|
||||
@@ -159,34 +191,46 @@ export class LinkModel<T extends LinkModelListener = LinkModelListener> extends
|
||||
}
|
||||
|
||||
addLabel(label: LabelModel) {
|
||||
this.labels.addEntity(label);
|
||||
label.setParent(this);
|
||||
this.labels.push(label);
|
||||
}
|
||||
|
||||
getPoints(): PointModel[] {
|
||||
return this.points.getArray();
|
||||
return this.points;
|
||||
}
|
||||
|
||||
setPoints(points: PointModel[]) {
|
||||
_.forEach(points, point => {
|
||||
point.setLink(this);
|
||||
point.setParent(this);
|
||||
});
|
||||
this.points.addEntities(points);
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
removePoint(pointModel: PointModel) {
|
||||
this.points.removeEntity(pointModel);
|
||||
this.points.splice(this.getPointIndex(pointModel), 1);
|
||||
}
|
||||
|
||||
removePointsBefore(pointModel: PointModel) {
|
||||
this.points.splice(0, this.getPointIndex(pointModel));
|
||||
}
|
||||
|
||||
removePointsAfter(pointModel: PointModel) {
|
||||
this.points.splice(this.getPointIndex(pointModel) + 1);
|
||||
}
|
||||
|
||||
removeMiddlePoints() {
|
||||
if (this.points.length > 2) {
|
||||
this.points.splice(0, this.points.length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
addPoint<P extends PointModel>(pointModel: P, index = 1): P {
|
||||
pointModel.setLink(this);
|
||||
this.points.addEntity(pointModel, index);
|
||||
pointModel.setParent(this);
|
||||
this.points.splice(index, 0, pointModel);
|
||||
return pointModel;
|
||||
}
|
||||
|
||||
generatePoint(x: number, y: number): PointModel {
|
||||
let point = new PointModel(this);
|
||||
point.getPoint().x = x;
|
||||
point.getPoint().y = y;
|
||||
return point;
|
||||
generatePoint(x: number = 0, y: number = 0): PointModel {
|
||||
return new PointModel(this, { x: x, y: y });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,131 @@
|
||||
import { BaseEvent } from "../BaseEntity";
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { LinkModel, LinkModelListener } from "./LinkModel";
|
||||
import { PortModel } from "./PortModel";
|
||||
import * as _ from "lodash";
|
||||
import { Rectangle, CanvasElementModel, GraphModel, DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { DiagramModel } from "./DiagramModel";
|
||||
import { PointModel } from "./PointModel";
|
||||
|
||||
export class NodeModel<T extends PortModel = PortModel> extends CanvasElementModel {
|
||||
protected dimensions: Rectangle;
|
||||
protected ports: GraphModel<T, null>;
|
||||
export interface NodeModelListener extends BaseModelListener {
|
||||
positionChanged?(event: BaseEvent<NodeModel>): void;
|
||||
}
|
||||
|
||||
constructor(nodeType: string = "default") {
|
||||
super(nodeType);
|
||||
this.dimensions = new Rectangle(0, 0, 0, 0);
|
||||
this.ports = new GraphModel("ports");
|
||||
export class NodeModel<T extends NodeModelListener = NodeModelListener> extends BaseModel<DiagramModel, T> {
|
||||
x: number;
|
||||
y: number;
|
||||
extras: any;
|
||||
ports: { [s: string]: PortModel };
|
||||
|
||||
// calculated post rendering so routing can be done correctly
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(nodeType: string = "default", id?: string) {
|
||||
super(nodeType, id);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.extras = {};
|
||||
this.ports = {};
|
||||
}
|
||||
|
||||
setDimensions(dimensions: Rectangle) {
|
||||
this.dimensions = dimensions;
|
||||
setPosition(x, y) {
|
||||
//store position
|
||||
let oldX = this.x;
|
||||
let oldY = this.y;
|
||||
_.forEach(this.ports, port => {
|
||||
_.forEach(port.getLinks(), link => {
|
||||
let point = link.getPointForPort(port);
|
||||
point.x = point.x + x - oldX;
|
||||
point.y = point.y + y - oldY;
|
||||
});
|
||||
});
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
getDimensions(): Rectangle {
|
||||
return this.dimensions;
|
||||
positionChanged() {
|
||||
this.iterateListeners(
|
||||
(listener: NodeModelListener, event) => listener.positionChanged && listener.positionChanged(event)
|
||||
);
|
||||
}
|
||||
|
||||
setPosition(x:number, y:number){
|
||||
this.dimensions.updateDimensions(x,y, this.dimensions.getWidth(), this.dimensions.getHeight());
|
||||
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(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.dimensions.deserialize(event.data.dimensions);
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.x = ob.x;
|
||||
this.y = ob.y;
|
||||
this.extras = ob.extras;
|
||||
|
||||
//deserialize ports
|
||||
let ports = event.subset("ports");
|
||||
_.forEach(ports.data, (port: any, index) => {
|
||||
let portOb = event.engine.getFactory(port.type).generateModel() as T;
|
||||
portOb.deSerialize(ports.subset(index));
|
||||
_.forEach(ob.ports, (port: any) => {
|
||||
let portOb = engine.getPortFactory(port.type).getNewInstance();
|
||||
portOb.deSerialize(port, engine);
|
||||
this.addPort(portOb);
|
||||
});
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return _.merge(super.serialize(), {
|
||||
dimensions: this.dimensions.serialize(),
|
||||
ports: this.ports.serialize()
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
extras: this.extras,
|
||||
ports: _.map(this.ports, port => {
|
||||
return port.serialize();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
getPortFromID(id): T | null {
|
||||
for (let i in this.ports) {
|
||||
if (this.ports[i].getID() === id) {
|
||||
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].id === id) {
|
||||
return this.ports[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getPort(name: string): T | null {
|
||||
return this.ports.getEntities()[name];
|
||||
getPort(name: string): PortModel | null {
|
||||
return this.ports[name];
|
||||
}
|
||||
|
||||
getPorts(): { [s: string]: T } {
|
||||
return this.ports.getEntities();
|
||||
getPorts(): { [s: string]: PortModel } {
|
||||
return this.ports;
|
||||
}
|
||||
|
||||
removePort(port: T) {
|
||||
removePort(port: PortModel) {
|
||||
//clear the parent node reference
|
||||
if (this.ports[port.name]) {
|
||||
this.ports[port.name].setParent(null);
|
||||
@@ -69,8 +133,14 @@ export class NodeModel<T extends PortModel = PortModel> extends CanvasElementMod
|
||||
}
|
||||
}
|
||||
|
||||
addPort(port: T): T {
|
||||
this.ports.addEntity(port);
|
||||
addPort<T extends PortModel>(port: T): T {
|
||||
port.setParent(this);
|
||||
this.ports[port.name] = port;
|
||||
return port;
|
||||
}
|
||||
|
||||
updateDimensions({ width, height }: { width: number; height: number }) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,69 @@
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { LinkModel } from "./LinkModel";
|
||||
import * as _ from "lodash";
|
||||
import {
|
||||
Point,
|
||||
CanvasElementModel,
|
||||
CanvasElementModelListener,
|
||||
Rectangle,
|
||||
DeserializeEvent
|
||||
} from "@projectstorm/react-canvas";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
|
||||
export class PointModel extends CanvasElementModel<CanvasElementModelListener> {
|
||||
protected point: Point;
|
||||
protected link: LinkModel;
|
||||
export class PointModel extends BaseModel<LinkModel, BaseModelListener> {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
constructor(link: LinkModel) {
|
||||
super("point");
|
||||
this.link = link;
|
||||
constructor(link: LinkModel, points: { x: number; y: number }) {
|
||||
super();
|
||||
this.x = points.x;
|
||||
this.y = points.y;
|
||||
this.parent = link;
|
||||
}
|
||||
|
||||
setDimensions(dimensions: Rectangle) {
|
||||
this.point = dimensions.getTopLeft();
|
||||
}
|
||||
|
||||
getDimensions(): Rectangle {
|
||||
return new Rectangle(this.point, 10, 10);
|
||||
getSelectedEntities() {
|
||||
if (super.isSelected() && !this.isConnectedToPort()) {
|
||||
return [this];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
isConnectedToPort(): boolean {
|
||||
return this.link.getPortForPoint(this) !== null;
|
||||
}
|
||||
|
||||
setLink(link: LinkModel) {
|
||||
this.link = link;
|
||||
return this.parent.getPortForPoint(this) !== null;
|
||||
}
|
||||
|
||||
getLink(): LinkModel {
|
||||
return this.link;
|
||||
return this.getParent();
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.point = new Point(event.data["x"], event.data["y"]);
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.x = ob.x;
|
||||
this.y = ob.y;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return _.merge(super.serialize(), {
|
||||
x: this.point.x,
|
||||
y: this.point.y
|
||||
x: this.x,
|
||||
y: this.y
|
||||
});
|
||||
}
|
||||
|
||||
remove() {
|
||||
//clear references
|
||||
if (this.link) {
|
||||
this.link.removePoint(this);
|
||||
if (this.parent) {
|
||||
this.parent.removePoint(this);
|
||||
}
|
||||
super.remove();
|
||||
}
|
||||
|
||||
getPoint(): Point {
|
||||
return this.point;
|
||||
updateLocation(points: { x: number; y: number }) {
|
||||
this.x = points.x;
|
||||
this.y = points.y;
|
||||
}
|
||||
|
||||
getX(): number {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
getY(): number {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
isLocked() {
|
||||
return super.isLocked() || this.getParent().isLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
import { BaseModel, BaseListener, DeserializeEvent } from "@projectstorm/react-canvas";
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { NodeModel } from "./NodeModel";
|
||||
import { LinkModel } from "./LinkModel";
|
||||
import * as _ from "lodash";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
|
||||
export class PortModel extends BaseModel<NodeModel, BaseListener> {
|
||||
export class PortModel extends BaseModel<NodeModel, BaseModelListener> {
|
||||
name: string;
|
||||
links: { [id: string]: LinkModel };
|
||||
maximumLinks: number;
|
||||
|
||||
constructor(name: string, type?: string, maximumLinks?: number) {
|
||||
super(type);
|
||||
// calculated post rendering so routing can be done correctly
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(name: string, type?: string, id?: string, maximumLinks?: number) {
|
||||
super(type, id);
|
||||
this.name = name;
|
||||
this.links = {};
|
||||
this.maximumLinks = maximumLinks;
|
||||
}
|
||||
|
||||
deSerialize(event: DeserializeEvent) {
|
||||
super.deSerialize(event);
|
||||
this.name = event.data.name;
|
||||
this.maximumLinks = event.data.maximumLinks;
|
||||
deSerialize(ob, engine: DiagramEngine) {
|
||||
super.deSerialize(ob, engine);
|
||||
this.name = ob.name;
|
||||
this.maximumLinks = ob.maximumLinks;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return _.merge(super.serialize(), {
|
||||
name: this.name,
|
||||
parentNode: this.parent.getID(),
|
||||
parentNode: this.parent.id,
|
||||
links: _.map(this.links, link => {
|
||||
return link.getID();
|
||||
return link.id;
|
||||
}),
|
||||
maximumLinks: this.maximumLinks
|
||||
});
|
||||
}
|
||||
|
||||
doClone(lookupTable = {}, clone) {
|
||||
clone.links = {};
|
||||
clone.parentNode = this.getParent().clone(lookupTable);
|
||||
}
|
||||
|
||||
getNode(): NodeModel {
|
||||
return this.getParent();
|
||||
}
|
||||
@@ -72,7 +84,18 @@ export class PortModel extends BaseModel<NodeModel, BaseListener> {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateCoords({ x, y, width, height }: { x: number; y: number; width: number; height: number }) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
canLinkToPort(port: PortModel): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isLocked() {
|
||||
return super.isLocked() || this.getParent().isLocked();
|
||||
}
|
||||
}
|
||||
|
||||
8
src/models/SelectionModel.ts
Normal file
8
src/models/SelectionModel.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BaseModel, BaseModelListener } from "./BaseModel";
|
||||
import { BaseEntity } from "../BaseEntity";
|
||||
|
||||
export interface SelectionModel {
|
||||
model: BaseModel<BaseEntity, BaseModelListener>;
|
||||
initialX: number;
|
||||
initialY: number;
|
||||
}
|
||||
133
src/routing/PathFinding.ts
Normal file
133
src/routing/PathFinding.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import * as PF from "pathfinding";
|
||||
import { DiagramEngine } from "../main";
|
||||
|
||||
/*
|
||||
it can be very expensive to calculate routes when every single pixel on the canvas
|
||||
is individually represented. Using the factor below, we combine values in order
|
||||
to achieve the best trade-off between accuracy and performance.
|
||||
*/
|
||||
export const ROUTING_SCALING_FACTOR = 5;
|
||||
|
||||
const pathFinderInstance = new PF.JumpPointFinder({
|
||||
heuristic: PF.Heuristic.manhattan,
|
||||
diagonalMovement: PF.DiagonalMovement.Never
|
||||
});
|
||||
|
||||
export default class PathFinding {
|
||||
instance: any;
|
||||
diagramEngine: DiagramEngine;
|
||||
|
||||
constructor(diagramEngine: DiagramEngine) {
|
||||
this.instance = pathFinderInstance;
|
||||
this.diagramEngine = diagramEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taking as argument a fully unblocked walking matrix, this method
|
||||
* finds a direct path from point A to B.
|
||||
*/
|
||||
calculateDirectPath(
|
||||
from: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
to: {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
): number[][] {
|
||||
const matrix = this.diagramEngine.getCanvasMatrix();
|
||||
const grid = new PF.Grid(matrix);
|
||||
|
||||
return pathFinderInstance.findPath(
|
||||
this.diagramEngine.translateRoutingX(Math.floor(from.x / ROUTING_SCALING_FACTOR)),
|
||||
this.diagramEngine.translateRoutingY(Math.floor(from.y / ROUTING_SCALING_FACTOR)),
|
||||
this.diagramEngine.translateRoutingX(Math.floor(to.x / ROUTING_SCALING_FACTOR)),
|
||||
this.diagramEngine.translateRoutingY(Math.floor(to.y / ROUTING_SCALING_FACTOR)),
|
||||
grid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Using @link{#calculateDirectPath}'s result as input, we here
|
||||
* determine the first walkable point found in the matrix that includes
|
||||
* blocked paths.
|
||||
*/
|
||||
calculateLinkStartEndCoords(
|
||||
matrix: number[][],
|
||||
path: number[][]
|
||||
): {
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
pathToStart: number[][];
|
||||
pathToEnd: number[][];
|
||||
} {
|
||||
const startIndex = path.findIndex(point => matrix[point[1]][point[0]] === 0);
|
||||
const endIndex =
|
||||
path.length -
|
||||
1 -
|
||||
path
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex(point => matrix[point[1]][point[0]] === 0);
|
||||
|
||||
// are we trying to create a path exclusively through blocked areas?
|
||||
// if so, let's fallback to the linear routing
|
||||
if (startIndex === -1 || endIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pathToStart = path.slice(0, startIndex);
|
||||
const pathToEnd = path.slice(endIndex);
|
||||
|
||||
return {
|
||||
start: {
|
||||
x: path[startIndex][0],
|
||||
y: path[startIndex][1]
|
||||
},
|
||||
end: {
|
||||
x: path[endIndex][0],
|
||||
y: path[endIndex][1]
|
||||
},
|
||||
pathToStart,
|
||||
pathToEnd
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts everything together: merges the paths from/to the centre of the ports,
|
||||
* with the path calculated around other elements.
|
||||
*/
|
||||
calculateDynamicPath(
|
||||
routingMatrix: number[][],
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
pathToStart: number[][],
|
||||
pathToEnd: number[][]
|
||||
) {
|
||||
// generate the path based on the matrix with obstacles
|
||||
const grid = new PF.Grid(routingMatrix);
|
||||
const dynamicPath = pathFinderInstance.findPath(start.x, start.y, end.x, end.y, grid);
|
||||
|
||||
// aggregate everything to have the calculated path ready for rendering
|
||||
const pathCoords = pathToStart
|
||||
.concat(dynamicPath, pathToEnd)
|
||||
.map(coords => [
|
||||
this.diagramEngine.translateRoutingX(coords[0], true),
|
||||
this.diagramEngine.translateRoutingY(coords[1], true)
|
||||
]);
|
||||
return PF.Util.compressPath(pathCoords);
|
||||
}
|
||||
}
|
||||
11
src/sass/_LinkLayerWidget.scss
Normal file
11
src/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
src/sass/_NodeLayerWidget.scss
Normal file
11
src/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%;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
@import "DiagramWidget";
|
||||
@import "LinkLayerWidget";
|
||||
@import "NodeLayerWidget";
|
||||
@import "NodeWidget";
|
||||
@import "PortWidget";
|
||||
|
||||
@@ -6,4 +8,4 @@
|
||||
@import "defaults/DefaultNodeWidget";
|
||||
@import "defaults/DefaultPortWidget";
|
||||
@import "defaults/DefaultLabelWidget";
|
||||
@import "defaults/DefaultLinkWidget";
|
||||
@import "defaults/DefaultLinkWidget";
|
||||
44
src/widgets/BaseWidget.tsx
Normal file
44
src/widgets/BaseWidget.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import * as _ from "lodash";
|
||||
|
||||
export interface BaseWidgetProps {
|
||||
/**
|
||||
* Override the base class name
|
||||
*/
|
||||
baseClass?: string;
|
||||
/**
|
||||
* append additional classes
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Additional props to add
|
||||
*/
|
||||
extraProps?: any;
|
||||
}
|
||||
|
||||
export class BaseWidget<P extends BaseWidgetProps = BaseWidgetProps, S = any> extends React.Component<P, S> {
|
||||
className: string;
|
||||
|
||||
constructor(name: string, props: P) {
|
||||
super(props);
|
||||
this.className = name;
|
||||
}
|
||||
|
||||
bem(selector: string): string {
|
||||
return (this.props.baseClass || this.className) + selector + " ";
|
||||
}
|
||||
|
||||
getClassName(): string {
|
||||
return (
|
||||
(this.props.baseClass || this.className) + " " + (this.props.className ? this.props.className + " " : "")
|
||||
);
|
||||
}
|
||||
|
||||
getProps(): any {
|
||||
return {
|
||||
...((this.props.extraProps as any) || {}),
|
||||
className: this.getClassName()
|
||||
};
|
||||
}
|
||||
}
|
||||
557
src/widgets/DiagramWidget.tsx
Normal file
557
src/widgets/DiagramWidget.tsx
Normal file
@@ -0,0 +1,557 @@
|
||||
import * as React from "react";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import * as _ from "lodash";
|
||||
import { LinkLayerWidget } from "./layers/LinkLayerWidget";
|
||||
import { NodeLayerWidget } from "./layers/NodeLayerWidget";
|
||||
import { Toolkit } from "../Toolkit";
|
||||
import { BaseAction } from "../actions/BaseAction";
|
||||
import { MoveCanvasAction } from "../actions/MoveCanvasAction";
|
||||
import { MoveItemsAction } from "../actions/MoveItemsAction";
|
||||
import { SelectingAction } from "../actions/SelectingAction";
|
||||
import { NodeModel } from "../models/NodeModel";
|
||||
import { PointModel } from "../models/PointModel";
|
||||
import { PortModel } from "../models/PortModel";
|
||||
import { LinkModel } from "../models/LinkModel";
|
||||
import { SelectionModel } from "../models/SelectionModel";
|
||||
import { BaseModel, BaseModelListener } from "../models/BaseModel";
|
||||
import { BaseEntity } from "../BaseEntity";
|
||||
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
|
||||
|
||||
export interface DiagramProps extends BaseWidgetProps {
|
||||
diagramEngine: DiagramEngine;
|
||||
|
||||
allowLooseLinks?: boolean;
|
||||
allowCanvasTranslation?: boolean;
|
||||
allowCanvasZoom?: boolean;
|
||||
inverseZoom?: boolean;
|
||||
maxNumberPointsPerLink?: number;
|
||||
smartRouting?: boolean;
|
||||
|
||||
actionStartedFiring?: (action: BaseAction) => boolean;
|
||||
actionStillFiring?: (action: BaseAction) => void;
|
||||
actionStoppedFiring?: (action: BaseAction) => void;
|
||||
|
||||
deleteKeys?: number[];
|
||||
}
|
||||
|
||||
export interface DiagramState {
|
||||
action: BaseAction | null;
|
||||
wasMoved: boolean;
|
||||
renderedNodes: boolean;
|
||||
windowListener: any;
|
||||
diagramEngineListener: any;
|
||||
document: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class DiagramWidget extends BaseWidget<DiagramProps, DiagramState> {
|
||||
public static defaultProps: DiagramProps = {
|
||||
diagramEngine: null,
|
||||
allowLooseLinks: true,
|
||||
allowCanvasTranslation: true,
|
||||
allowCanvasZoom: true,
|
||||
inverseZoom: false,
|
||||
maxNumberPointsPerLink: Infinity, // backwards compatible default
|
||||
smartRouting: false,
|
||||
deleteKeys: [46, 8]
|
||||
};
|
||||
|
||||
onKeyUpPointer: (this: Window, ev: KeyboardEvent) => void = null;
|
||||
|
||||
constructor(props: DiagramProps) {
|
||||
super("srd-diagram", props);
|
||||
this.onMouseMove = this.onMouseMove.bind(this);
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
this.state = {
|
||||
action: null,
|
||||
wasMoved: false,
|
||||
renderedNodes: false,
|
||||
windowListener: null,
|
||||
diagramEngineListener: null,
|
||||
document: null
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
|
||||
this.props.diagramEngine.setCanvas(null);
|
||||
window.removeEventListener("keyup", this.onKeyUpPointer);
|
||||
window.removeEventListener("mouseUp", this.onMouseUp);
|
||||
window.removeEventListener("mouseMove", this.onMouseMove);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: DiagramProps) {
|
||||
if (this.props.diagramEngine !== nextProps.diagramEngine) {
|
||||
this.props.diagramEngine.removeListener(this.state.diagramEngineListener);
|
||||
const diagramEngineListener = nextProps.diagramEngine.addListener({
|
||||
repaintCanvas: () => this.forceUpdate()
|
||||
});
|
||||
this.setState({ diagramEngineListener });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: DiagramProps) {
|
||||
if (this.props.diagramEngine.diagramModel.id !== nextProps.diagramEngine.diagramModel.id) {
|
||||
this.setState({ renderedNodes: false });
|
||||
nextProps.diagramEngine.diagramModel.rendered = true;
|
||||
}
|
||||
if (!nextProps.diagramEngine.diagramModel.rendered) {
|
||||
this.setState({ renderedNodes: false });
|
||||
nextProps.diagramEngine.diagramModel.rendered = true;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.renderedNodes) {
|
||||
this.setState({
|
||||
renderedNodes: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onKeyUpPointer = this.onKeyUp.bind(this);
|
||||
|
||||
//add a keyboard listener
|
||||
this.setState({
|
||||
document: document,
|
||||
renderedNodes: true,
|
||||
diagramEngineListener: this.props.diagramEngine.addListener({
|
||||
repaintCanvas: () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", this.onKeyUpPointer, false);
|
||||
|
||||
// dont focus the window when in test mode - jsdom fails
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a model and element under the mouse cursor
|
||||
*/
|
||||
getMouseElement(event): { model: BaseModel<BaseEntity, BaseModelListener>; element: Element } {
|
||||
var target = event.target as Element;
|
||||
var diagramModel = this.props.diagramEngine.diagramModel;
|
||||
|
||||
//is it a port
|
||||
var element = Toolkit.closest(target, ".port[data-name]");
|
||||
if (element) {
|
||||
var nodeElement = Toolkit.closest(target, ".node[data-nodeid]") as HTMLElement;
|
||||
return {
|
||||
model: diagramModel
|
||||
.getNode(nodeElement.getAttribute("data-nodeid"))
|
||||
.getPort(element.getAttribute("data-name")),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
//look for a point
|
||||
element = Toolkit.closest(target, ".point[data-id]");
|
||||
if (element) {
|
||||
return {
|
||||
model: diagramModel
|
||||
.getLink(element.getAttribute("data-linkid"))
|
||||
.getPointModel(element.getAttribute("data-id")),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
//look for a link
|
||||
element = Toolkit.closest(target, "[data-linkid]");
|
||||
if (element) {
|
||||
return {
|
||||
model: diagramModel.getLink(element.getAttribute("data-linkid")),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
//look for a node
|
||||
element = Toolkit.closest(target, ".node[data-nodeid]");
|
||||
if (element) {
|
||||
return {
|
||||
model: diagramModel.getNode(element.getAttribute("data-nodeid")),
|
||||
element: element
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fireAction() {
|
||||
if (this.state.action && this.props.actionStillFiring) {
|
||||
this.props.actionStillFiring(this.state.action);
|
||||
}
|
||||
}
|
||||
|
||||
stopFiringAction(shouldSkipEvent?: boolean) {
|
||||
if (this.props.actionStoppedFiring && !shouldSkipEvent) {
|
||||
this.props.actionStoppedFiring(this.state.action);
|
||||
}
|
||||
this.setState({ action: null });
|
||||
}
|
||||
|
||||
startFiringAction(action: BaseAction) {
|
||||
var setState = true;
|
||||
if (this.props.actionStartedFiring) {
|
||||
setState = this.props.actionStartedFiring(action);
|
||||
}
|
||||
if (setState) {
|
||||
this.setState({ action: action });
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(event) {
|
||||
var diagramEngine = this.props.diagramEngine;
|
||||
var diagramModel = diagramEngine.getDiagramModel();
|
||||
//select items so draw a bounding box
|
||||
if (this.state.action instanceof SelectingAction) {
|
||||
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
|
||||
|
||||
_.forEach(diagramModel.getNodes(), node => {
|
||||
if ((this.state.action as SelectingAction).containsElement(node.x, node.y, diagramModel)) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
_.forEach(diagramModel.getLinks(), link => {
|
||||
var allSelected = true;
|
||||
_.forEach(link.points, point => {
|
||||
if ((this.state.action as SelectingAction).containsElement(point.x, point.y, diagramModel)) {
|
||||
point.setSelected(true);
|
||||
} else {
|
||||
allSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allSelected) {
|
||||
link.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.state.action.mouseX2 = relative.x;
|
||||
this.state.action.mouseY2 = relative.y;
|
||||
|
||||
this.fireAction();
|
||||
this.setState({ action: this.state.action });
|
||||
return;
|
||||
} else if (this.state.action instanceof MoveItemsAction) {
|
||||
let amountX = event.clientX - this.state.action.mouseX;
|
||||
let amountY = event.clientY - this.state.action.mouseY;
|
||||
let amountZoom = diagramModel.getZoomLevel() / 100;
|
||||
|
||||
_.forEach(this.state.action.selectionModels, model => {
|
||||
// in this case we need to also work out the relative grid position
|
||||
if (
|
||||
model.model instanceof NodeModel ||
|
||||
(model.model instanceof PointModel && !model.model.isConnectedToPort())
|
||||
) {
|
||||
model.model.x = diagramModel.getGridPosition(model.initialX + amountX / amountZoom);
|
||||
model.model.y = diagramModel.getGridPosition(model.initialY + amountY / amountZoom);
|
||||
|
||||
if (model.model instanceof NodeModel) {
|
||||
model.model.positionChanged();
|
||||
|
||||
// update port coordinates as well
|
||||
_.forEach(model.model.getPorts(), port => {
|
||||
const portCoords = this.props.diagramEngine.getPortCoords(port);
|
||||
port.updateCoords(portCoords);
|
||||
});
|
||||
}
|
||||
|
||||
if (diagramEngine.isSmartRoutingEnabled()) {
|
||||
diagramEngine.calculateRoutingMatrix();
|
||||
}
|
||||
} 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.x = model.initialX + diagramModel.getGridPosition(amountX / amountZoom);
|
||||
model.model.y = model.initialY + diagramModel.getGridPosition(amountY / amountZoom);
|
||||
}
|
||||
});
|
||||
|
||||
if (diagramEngine.isSmartRoutingEnabled()) {
|
||||
diagramEngine.calculateCanvasMatrix();
|
||||
}
|
||||
|
||||
this.fireAction();
|
||||
if (!this.state.wasMoved) {
|
||||
this.setState({ wasMoved: true });
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
} else if (this.state.action instanceof MoveCanvasAction) {
|
||||
//translate the actual canvas
|
||||
if (this.props.allowCanvasTranslation) {
|
||||
diagramModel.setOffset(
|
||||
this.state.action.initialOffsetX + (event.clientX - this.state.action.mouseX),
|
||||
this.state.action.initialOffsetY + (event.clientY - this.state.action.mouseY)
|
||||
);
|
||||
this.fireAction();
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp(event) {
|
||||
//delete all selected
|
||||
if (this.props.deleteKeys.indexOf(event.keyCode) !== -1) {
|
||||
_.forEach(this.props.diagramEngine.getDiagramModel().getSelectedItems(), element => {
|
||||
//only delete items which are not locked
|
||||
if (!this.props.diagramEngine.isModelLocked(element)) {
|
||||
element.remove();
|
||||
}
|
||||
});
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(event) {
|
||||
var diagramEngine = this.props.diagramEngine;
|
||||
//are we going to connect a link to something?
|
||||
if (this.state.action instanceof MoveItemsAction) {
|
||||
var element = this.getMouseElement(event);
|
||||
_.forEach(this.state.action.selectionModels, model => {
|
||||
//only care about points connecting to things
|
||||
if (!(model.model instanceof PointModel)) {
|
||||
return;
|
||||
}
|
||||
if (element && element.model instanceof PortModel && !diagramEngine.isModelLocked(element.model)) {
|
||||
let link = model.model.getLink();
|
||||
if (link.getTargetPort() !== null) {
|
||||
//if this was a valid link already and we are adding a node in the middle, create 2 links from the original
|
||||
if (link.getTargetPort() !== element.model && link.getSourcePort() !== element.model) {
|
||||
const targetPort = link.getTargetPort();
|
||||
let newLink = link.clone({});
|
||||
newLink.setSourcePort(element.model);
|
||||
newLink.setTargetPort(targetPort);
|
||||
link.setTargetPort(element.model);
|
||||
targetPort.removeLink(link);
|
||||
newLink.removePointsBefore(newLink.getPoints()[link.getPointIndex(model.model)]);
|
||||
link.removePointsAfter(model.model);
|
||||
diagramEngine.getDiagramModel().addLink(newLink);
|
||||
//if we are connecting to the same target or source, remove tweener points
|
||||
} else if (link.getTargetPort() === element.model) {
|
||||
link.removePointsAfter(model.model);
|
||||
} else if (link.getSourcePort() === element.model) {
|
||||
link.removePointsBefore(model.model);
|
||||
}
|
||||
} else {
|
||||
link.setTargetPort(element.model);
|
||||
}
|
||||
delete this.props.diagramEngine.linksThatHaveInitiallyRendered[link.getID()];
|
||||
}
|
||||
});
|
||||
|
||||
//check for / remove any loose links in any models which have been moved
|
||||
if (!this.props.allowLooseLinks && this.state.wasMoved) {
|
||||
_.forEach(this.state.action.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.state.action.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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
diagramEngine.clearRepaintEntities();
|
||||
this.stopFiringAction(!this.state.wasMoved);
|
||||
} else {
|
||||
diagramEngine.clearRepaintEntities();
|
||||
this.stopFiringAction();
|
||||
}
|
||||
this.state.document.removeEventListener("mousemove", this.onMouseMove);
|
||||
this.state.document.removeEventListener("mouseup", this.onMouseUp);
|
||||
}
|
||||
|
||||
drawSelectionBox() {
|
||||
let dimensions = (this.state.action as SelectingAction).getBoxDimensions();
|
||||
return (
|
||||
<div
|
||||
className={this.bem("__selector")}
|
||||
style={{
|
||||
top: dimensions.top,
|
||||
left: dimensions.left,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var diagramEngine = this.props.diagramEngine;
|
||||
diagramEngine.setMaxNumberPointsPerLink(this.props.maxNumberPointsPerLink);
|
||||
diagramEngine.setSmartRoutingStatus(this.props.smartRouting);
|
||||
var diagramModel = diagramEngine.getDiagramModel();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...this.getProps()}
|
||||
ref={ref => {
|
||||
if (ref) {
|
||||
this.props.diagramEngine.setCanvas(ref);
|
||||
}
|
||||
}}
|
||||
onWheel={event => {
|
||||
if (this.props.allowCanvasZoom) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const oldZoomFactor = diagramModel.getZoomLevel() / 100;
|
||||
let scrollDelta = this.props.inverseZoom ? -event.deltaY : event.deltaY;
|
||||
//check if it is pinch gesture
|
||||
if (event.ctrlKey && scrollDelta % 1 !== 0) {
|
||||
/*Chrome and Firefox sends wheel event with deltaY that
|
||||
have fractional part, also `ctrlKey` prop of the event is true
|
||||
though ctrl isn't pressed
|
||||
*/
|
||||
scrollDelta /= 3;
|
||||
} else {
|
||||
scrollDelta /= 60;
|
||||
}
|
||||
if (diagramModel.getZoomLevel() + scrollDelta > 10) {
|
||||
diagramModel.setZoomLevel(diagramModel.getZoomLevel() + scrollDelta);
|
||||
}
|
||||
|
||||
const zoomFactor = diagramModel.getZoomLevel() / 100;
|
||||
|
||||
const boundingRect = event.currentTarget.getBoundingClientRect();
|
||||
const clientWidth = boundingRect.width;
|
||||
const clientHeight = boundingRect.height;
|
||||
// compute difference between rect before and after scroll
|
||||
const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor;
|
||||
const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor;
|
||||
// compute mouse coords relative to canvas
|
||||
const clientX = event.clientX - boundingRect.left;
|
||||
const clientY = event.clientY - boundingRect.top;
|
||||
|
||||
// compute width and height increment factor
|
||||
const xFactor = (clientX - diagramModel.getOffsetX()) / oldZoomFactor / clientWidth;
|
||||
const yFactor = (clientY - diagramModel.getOffsetY()) / oldZoomFactor / clientHeight;
|
||||
|
||||
diagramModel.setOffset(
|
||||
diagramModel.getOffsetX() - widthDiff * xFactor,
|
||||
diagramModel.getOffsetY() - heightDiff * yFactor
|
||||
);
|
||||
|
||||
diagramEngine.enableRepaintEntities([]);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}}
|
||||
onMouseDown={event => {
|
||||
if (event.nativeEvent.which === 3) return;
|
||||
this.setState({ ...this.state, wasMoved: false });
|
||||
|
||||
diagramEngine.clearRepaintEntities();
|
||||
var model = this.getMouseElement(event);
|
||||
//the canvas was selected
|
||||
if (model === null) {
|
||||
//is it a multiple selection
|
||||
if (event.shiftKey) {
|
||||
var relative = diagramEngine.getRelativePoint(event.clientX, event.clientY);
|
||||
this.startFiringAction(new SelectingAction(relative.x, relative.y));
|
||||
} else {
|
||||
//its a drag the canvas event
|
||||
diagramModel.clearSelection();
|
||||
this.startFiringAction(new MoveCanvasAction(event.clientX, event.clientY, diagramModel));
|
||||
}
|
||||
} else if (model.model instanceof PortModel) {
|
||||
//its a port element, we want to drag a link
|
||||
if (!this.props.diagramEngine.isModelLocked(model.model)) {
|
||||
var relative = diagramEngine.getRelativeMousePoint(event);
|
||||
var sourcePort = model.model;
|
||||
var link = sourcePort.createLinkModel();
|
||||
link.setSourcePort(sourcePort);
|
||||
|
||||
if (link) {
|
||||
link.removeMiddlePoints();
|
||||
if (link.getSourcePort() !== sourcePort) {
|
||||
link.setSourcePort(sourcePort);
|
||||
}
|
||||
link.setTargetPort(null);
|
||||
|
||||
link.getFirstPoint().updateLocation(relative);
|
||||
link.getLastPoint().updateLocation(relative);
|
||||
|
||||
diagramModel.clearSelection();
|
||||
link.getLastPoint().setSelected(true);
|
||||
diagramModel.addLink(link);
|
||||
|
||||
this.startFiringAction(
|
||||
new MoveItemsAction(event.clientX, event.clientY, diagramEngine)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
diagramModel.clearSelection();
|
||||
}
|
||||
} else {
|
||||
//its some or other element, probably want to move it
|
||||
if (!event.shiftKey && !model.model.isSelected()) {
|
||||
diagramModel.clearSelection();
|
||||
}
|
||||
model.model.setSelected(true);
|
||||
|
||||
this.startFiringAction(new MoveItemsAction(event.clientX, event.clientY, diagramEngine));
|
||||
}
|
||||
this.state.document.addEventListener("mousemove", this.onMouseMove);
|
||||
this.state.document.addEventListener("mouseup", this.onMouseUp);
|
||||
}}
|
||||
>
|
||||
{this.state.renderedNodes && (
|
||||
<LinkLayerWidget
|
||||
diagramEngine={diagramEngine}
|
||||
pointAdded={(point: PointModel, event) => {
|
||||
this.state.document.addEventListener("mousemove", this.onMouseMove);
|
||||
this.state.document.addEventListener("mouseup", this.onMouseUp);
|
||||
event.stopPropagation();
|
||||
diagramModel.clearSelection(point);
|
||||
this.setState({
|
||||
action: new MoveItemsAction(event.clientX, event.clientY, diagramEngine)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NodeLayerWidget diagramEngine={diagramEngine} />
|
||||
{this.state.action instanceof SelectingAction && this.drawSelectionBox()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/widgets/LinkWidget.tsx
Normal file
30
src/widgets/LinkWidget.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { LinkModel } from "../models/LinkModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
|
||||
|
||||
export interface LinkProps extends BaseWidgetProps {
|
||||
link: LinkModel;
|
||||
diagramEngine: DiagramEngine;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export interface LinkState {}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class LinkWidget extends BaseWidget<LinkProps, LinkState> {
|
||||
constructor(props: LinkProps) {
|
||||
super("srd-link", props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return this.props.diagramEngine.canEntityRepaint(this.props.link);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { DiagramEngine } from "../DiagramEngine";
|
||||
import { NodeModel } from "../models/NodeModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { Toolkit } from "../Toolkit";
|
||||
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
|
||||
|
||||
export interface NodeProps extends BaseWidgetProps {
|
||||
node: NodeModel;
|
||||
@@ -11,12 +12,19 @@ export interface NodeProps extends BaseWidgetProps {
|
||||
|
||||
export interface NodeState {}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class NodeWidget extends BaseWidget<NodeProps, NodeState> {
|
||||
constructor(props: NodeProps) {
|
||||
super("srd-node", props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return this.props.diagramEngine.canEntityRepaint(this.props.node);
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
return "node " + super.getClassName() + (this.props.node.isSelected() ? this.bem("--selected") : "");
|
||||
}
|
||||
@@ -25,10 +33,10 @@ export class NodeWidget extends BaseWidget<NodeProps, NodeState> {
|
||||
return (
|
||||
<div
|
||||
{...this.getProps()}
|
||||
data-nodeid={this.props.node.getID()}
|
||||
data-nodeid={this.props.node.id}
|
||||
style={{
|
||||
top: this.props.node.getDimensions().getTopLeft().y,
|
||||
left: this.props.node.getDimensions().getTopLeft().x
|
||||
top: this.props.node.y,
|
||||
left: this.props.node.x
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { NodeModel } from "../models/NodeModel";
|
||||
import { BaseWidget, BaseWidgetProps } from "@projectstorm/react-canvas";
|
||||
import { BaseWidget, BaseWidgetProps } from "./BaseWidget";
|
||||
|
||||
export interface PortProps extends BaseWidgetProps {
|
||||
name: string;
|
||||
@@ -11,6 +11,9 @@ export interface PortState {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class PortWidget extends BaseWidget<PortProps, PortState> {
|
||||
constructor(props: PortProps) {
|
||||
super("srd-port", props);
|
||||
|
||||
92
src/widgets/layers/LinkLayerWidget.tsx
Normal file
92
src/widgets/layers/LinkLayerWidget.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
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";
|
||||
|
||||
export interface LinkLayerProps extends BaseWidgetProps {
|
||||
diagramEngine: DiagramEngine;
|
||||
pointAdded: (point: PointModel, event: MouseEvent) => any;
|
||||
}
|
||||
|
||||
export interface LinkLayerState {}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
export class LinkLayerWidget extends BaseWidget<LinkLayerProps, LinkLayerState> {
|
||||
constructor(props: LinkLayerProps) {
|
||||
super("srd-link-layer", props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
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
|
||||
this.props.diagramEngine.canvas &&
|
||||
_.map(diagramModel.getLinks(), link => {
|
||||
if (
|
||||
this.props.diagramEngine.nodesRendered &&
|
||||
!this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id]
|
||||
) {
|
||||
if (link.sourcePort !== null) {
|
||||
try {
|
||||
const portCenter = this.props.diagramEngine.getPortCenter(link.sourcePort);
|
||||
link.points[0].updateLocation(portCenter);
|
||||
|
||||
const portCoords = this.props.diagramEngine.getPortCoords(link.sourcePort);
|
||||
link.sourcePort.updateCoords(portCoords);
|
||||
|
||||
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
|
||||
} catch (ignore) {
|
||||
/*noop*/
|
||||
}
|
||||
}
|
||||
if (link.targetPort !== null) {
|
||||
try {
|
||||
const portCenter = this.props.diagramEngine.getPortCenter(link.targetPort);
|
||||
_.last(link.points).updateLocation(portCenter);
|
||||
|
||||
const portCoords = this.props.diagramEngine.getPortCoords(link.targetPort);
|
||||
link.targetPort.updateCoords(portCoords);
|
||||
|
||||
this.props.diagramEngine.linksThatHaveInitiallyRendered[link.id] = true;
|
||||
} catch (ignore) {
|
||||
/*noop*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//generate links
|
||||
var generatedLink = this.props.diagramEngine.generateWidgetForLink(link);
|
||||
if (!generatedLink) {
|
||||
throw new Error(`no link generated for type: ${link.getType()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkWidget key={link.getID()} link={link} diagramEngine={this.props.diagramEngine}>
|
||||
{React.cloneElement(generatedLink, {
|
||||
pointAdded: this.props.pointAdded
|
||||
})}
|
||||
</LinkWidget>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
64
src/widgets/layers/NodeLayerWidget.tsx
Normal file
64
src/widgets/layers/NodeLayerWidget.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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 interface NodeLayerState {}
|
||||
|
||||
export class NodeLayerWidget extends BaseWidget<NodeLayerProps, NodeLayerState> {
|
||||
constructor(props: NodeLayerProps) {
|
||||
super("srd-node-layer", props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
updateNodeDimensions = () => {
|
||||
if (!this.props.diagramEngine.nodesRendered) {
|
||||
const diagramModel = this.props.diagramEngine.getDiagramModel();
|
||||
_.map(diagramModel.getNodes(), node => {
|
||||
node.updateDimensions(this.props.diagramEngine.getNodeDimensions(node));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateNodeDimensions();
|
||||
this.props.diagramEngine.nodesRendered = true;
|
||||
}
|
||||
|
||||
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 React.createElement(
|
||||
NodeWidget,
|
||||
{
|
||||
diagramEngine: this.props.diagramEngine,
|
||||
key: node.id,
|
||||
node: node
|
||||
},
|
||||
this.props.diagramEngine.generateWidgetForNode(node)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export class E2EPort extends E2EElement {
|
||||
// click on this port
|
||||
this.page.mouse.move(bounds.x, bounds.y);
|
||||
this.page.mouse.down();
|
||||
|
||||
//
|
||||
let bounds2 = await port.element.boundingBox();
|
||||
|
||||
// drag to other port
|
||||
@@ -72,10 +72,13 @@ export class E2EPort extends E2EElement {
|
||||
this.page.mouse.move(x, y);
|
||||
this.page.mouse.up();
|
||||
|
||||
const link = _.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0];
|
||||
if(!link){
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the parent to get the link
|
||||
return await this.helper.link(
|
||||
_.difference(_.flatMap((await this.parent.model()).ports, "links"), currentLinks)[0]
|
||||
);
|
||||
return await this.helper.link(link);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +101,11 @@ export class E2ELink extends E2EElement {
|
||||
async select(): Promise<any> {
|
||||
const point = await this.page.evaluate(id => {
|
||||
const path = document.querySelector(`path[data-linkid="${id}"]`) as SVGPathElement;
|
||||
return path.getPointAtLength(path.getTotalLength() / 2);
|
||||
const point =path.getPointAtLength(path.getTotalLength() / 2);
|
||||
return {
|
||||
x: point.x,
|
||||
y: point.y
|
||||
}
|
||||
}, this.id);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.page.mouse.move(point.x, point.y);
|
||||
@@ -115,11 +122,19 @@ export class E2EHelper {
|
||||
}
|
||||
|
||||
async link(id): Promise<E2ELink> {
|
||||
if(!id){
|
||||
throw "Link ID must be valid"
|
||||
}
|
||||
let selector = await this.page.waitForSelector(`path[data-linkid="${id}"]`);
|
||||
return new E2ELink(this, this.page, selector, id);
|
||||
}
|
||||
|
||||
async node(id): Promise<E2ENode> {
|
||||
if(!id){
|
||||
if(!id){
|
||||
throw "Node ID must be valid"
|
||||
}
|
||||
}
|
||||
let selector = await this.page.waitForSelector(`div[data-nodeid="${id}"]`);
|
||||
return new E2ENode(this, this.page, selector, id);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ glob.glob(__dirname + "/../../demos/demo-*/index.tsx", {}, (err, files) => {
|
||||
},
|
||||
};
|
||||
|
||||
console.log(config);
|
||||
|
||||
webpack(config, (err, stats) => {
|
||||
if (err || stats.hasErrors()) {
|
||||
// Handle errors here
|
||||
|
||||
@@ -1,60 +1,36 @@
|
||||
import "jest";
|
||||
import * as puppeteer from "puppeteer";
|
||||
import { E2EHelper } from "./E2EHelper";
|
||||
import {E2EHelper} from "./E2EHelper";
|
||||
|
||||
var browser;
|
||||
describe("simple flow test", () => {
|
||||
|
||||
async function itShould(demo: string, directive, test: (page: puppeteer.Page, helper: E2EHelper) => any) {
|
||||
it(directive, async () => {
|
||||
let page = await browser.newPage();
|
||||
await page.goto("file://" + __dirname + "/../../dist/e2e/" + demo + "/index.html");
|
||||
let helper = new E2EHelper(page);
|
||||
await test(page, helper);
|
||||
await page.close();
|
||||
beforeEach(async () => {
|
||||
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple-flow/index.html`);
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
if (process.env.CIRCLECI) {
|
||||
console.log("using CircleCI");
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
||||
});
|
||||
} else {
|
||||
browser = await puppeteer.launch({
|
||||
headless: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
describe("simple flow test", async () => {
|
||||
itShould("demo-simple-flow", "drag link to port adds a link", async (page, helper) => {
|
||||
it("drag link to port adds a link", async () => {
|
||||
// create a new link
|
||||
let node1 = await helper.node("6");
|
||||
let helper = new E2EHelper(page);
|
||||
let node1 = await helper.node("17");
|
||||
let node2 = await helper.node("9");
|
||||
|
||||
let port1 = await node1.port("7");
|
||||
let port1 = await node1.port("18");
|
||||
let port2 = await node2.port("10");
|
||||
|
||||
let newlink = await port1.link(port2);
|
||||
await expect(await newlink.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
itShould("demo-simple-flow", "drag link to node does not add a link", async (page, helper) => {
|
||||
it("drag link to node does not add a link", async () => {
|
||||
// create a new link
|
||||
let node1 = await helper.node("6");
|
||||
let helper = new E2EHelper(page);
|
||||
let node1 = await helper.node("17");
|
||||
let node2 = await helper.node("9");
|
||||
|
||||
let port1 = await node1.port("7");
|
||||
let port1 = await node1.port("18");
|
||||
|
||||
let node2Bounds = await node2.element.boundingBox();
|
||||
|
||||
let newlink = await port1.linkToPoint(node2Bounds.x, node2Bounds.y);
|
||||
await expect(await newlink.exists()).toBeFalsy();
|
||||
|
||||
await expect(newlink).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,21 @@
|
||||
import "jest";
|
||||
import * as puppeteer from "puppeteer";
|
||||
import { E2EHelper } from "./E2EHelper";
|
||||
import {E2EHelper} from "./E2EHelper";
|
||||
|
||||
var browser;
|
||||
describe("simple test", () => {
|
||||
|
||||
async function itShould(demo: string, directive, test: (page: puppeteer.Page, helper: E2EHelper) => any) {
|
||||
it(directive, async () => {
|
||||
let page = await browser.newPage();
|
||||
await page.goto("file://" + __dirname + "/../../dist/e2e/" + demo + "/index.html");
|
||||
let helper = new E2EHelper(page);
|
||||
await test(page, helper);
|
||||
await page.close();
|
||||
beforeAll(async () => {
|
||||
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple/index.html`)
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
if (process.env.CIRCLECI) {
|
||||
console.log("using CircleCI");
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
||||
});
|
||||
} else {
|
||||
browser = await puppeteer.launch({
|
||||
headless: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
describe("simple test", async () => {
|
||||
itShould("demo-simple", "should delete a link and create a new one", async (page, helper) => {
|
||||
it("should delete a link and create a new one", async () => {
|
||||
// get the existing link
|
||||
let helper = new E2EHelper(page);
|
||||
let link = await helper.link("12");
|
||||
await expect(await link.exists()).toBeTruthy();
|
||||
|
||||
// remove it
|
||||
await link.select();
|
||||
await page.keyboard.press("Del");
|
||||
await page.keyboard.press("Delete");
|
||||
|
||||
await expect(await link.exists()).toBeFalsy();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
||||
import initStoryshots from "@storybook/addon-storyshots";
|
||||
import "raf/polyfill";
|
||||
|
||||
initStoryshots({ configPath: __dirname });
|
||||
|
||||
|
||||
initStoryshots();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { storiesOf, addDecorator } from "@storybook/react";
|
||||
import { Toolkit } from "../../src/Toolkit";
|
||||
|
||||
Toolkit.TESTING = true;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"compilerOptions": {
|
||||
"suppressExcessPropertyErrors": true,
|
||||
"declaration": true,
|
||||
"outDir": "@types",
|
||||
"target": "es5",
|
||||
"outDir": "dist/@types",
|
||||
"strictNullChecks": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -14,11 +13,16 @@
|
||||
"paths": {
|
||||
"storm-react-diagrams": ["src/main.ts"]
|
||||
},
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
"es6"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.spec.ts",
|
||||
|
||||
31
tslint.json
31
tslint.json
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"member-access": false,
|
||||
"comment-format": false,
|
||||
"max-line-length": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"quotemark": [true, "double", "jsx-double"],
|
||||
"arrow-parens": false,
|
||||
"indent": [true, "tabs", 2],
|
||||
"semicolon": false,
|
||||
"object-literal-key-quotes": [true, "as-needed"],
|
||||
"no-var-keyword": false,
|
||||
"jsdoc-format": false,
|
||||
"prefer-const": false,
|
||||
"interface-name": false,
|
||||
"array-type": false,
|
||||
"trailing-comma": false,
|
||||
"one-line": false,
|
||||
"object-literal-shorthand": false,
|
||||
"no-string-literal": false,
|
||||
"ordered-imports": false,
|
||||
"prefer-for-of": false,
|
||||
"no-empty-interface": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
@@ -1,22 +1,7 @@
|
||||
const webpack = require("webpack");
|
||||
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
|
||||
var path = require("path");
|
||||
var plugins = [];
|
||||
const production = process.env.NODE_ENV === "production";
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
//do we minify it all
|
||||
if (production) {
|
||||
console.log("creating production build");
|
||||
plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": '"production"'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Dylan Vorster
|
||||
*/
|
||||
module.exports =
|
||||
//for building the umd distribution
|
||||
{
|
||||
@@ -27,27 +12,7 @@ module.exports =
|
||||
libraryTarget: "umd",
|
||||
library: "storm-react-diagrams"
|
||||
},
|
||||
externals: {
|
||||
react: {
|
||||
root: "React",
|
||||
commonjs2: "react",
|
||||
commonjs: "react",
|
||||
amd: "react"
|
||||
},
|
||||
"react-dom": {
|
||||
root: "ReactDOM",
|
||||
commonjs2: "react-dom",
|
||||
commonjs: "react-dom",
|
||||
amd: "react-dom"
|
||||
},
|
||||
lodash: {
|
||||
commonjs: "lodash",
|
||||
commonjs2: "lodash",
|
||||
amd: "_",
|
||||
root: "_"
|
||||
}
|
||||
},
|
||||
plugins: plugins,
|
||||
externals: [nodeExternals()],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -67,16 +32,6 @@ module.exports =
|
||||
devtool: production ? "source-map" : "cheap-module-source-map",
|
||||
mode: production ? "production" : "development",
|
||||
optimization: {
|
||||
minimizer: [
|
||||
// we specify a custom UglifyJsPlugin here to get source maps in production
|
||||
new UglifyJsPlugin({
|
||||
uglifyOptions: {
|
||||
compress: false,
|
||||
ecma: 5,
|
||||
mangle: false
|
||||
},
|
||||
sourceMap: true
|
||||
})
|
||||
]
|
||||
minimizer: [new TerserPlugin()],
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user