├── .circleci
└── config.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
├── react-flow-chart.iml
├── vcs.xml
└── workspace.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
└── storybook
│ ├── addons.js
│ ├── config.js
│ └── webpack.config.js
├── docs
├── favicon.ico
├── iframe.html
├── index.html
├── main.a04756a8b33b9f0f49ad.bundle.js
├── main.a0afedec1e09075b22ed.bundle.js
├── main.a0afedec1e09075b22ed.bundle.js.map
├── runtime~main.a0afedec1e09075b22ed.bundle.js
├── runtime~main.a0afedec1e09075b22ed.bundle.js.map
├── runtime~main.e3851caad73f000648d3.bundle.js
├── sb_dll
│ ├── storybook_ui-manifest.json
│ ├── storybook_ui_dll.LICENCE
│ └── storybook_ui_dll.js
├── vendors~main.7c18f3f0eabaaa53749a.bundle.js
├── vendors~main.a0afedec1e09075b22ed.bundle.js
├── vendors~main.a0afedec1e09075b22ed.bundle.js.LICENSE.txt
└── vendors~main.a0afedec1e09075b22ed.bundle.js.map
├── images
├── demo.gif
└── demo.png
├── package-lock.json
├── package.json
├── src
├── components
│ ├── Canvas
│ │ ├── Canvas.wrapper.tsx
│ │ ├── CanvasContext.ts
│ │ ├── CanvasInner.default.tsx
│ │ ├── CanvasOuter.default.tsx
│ │ └── index.tsx
│ ├── FlowChart
│ │ ├── FlowChart.tsx
│ │ ├── index.ts
│ │ └── utils
│ │ │ └── grid.ts
│ ├── Link
│ │ ├── Link.default.tsx
│ │ ├── Link.wrapper.tsx
│ │ ├── index.ts
│ │ └── utils
│ │ │ ├── generateCurvePath.ts
│ │ │ ├── getLinkPosition.ts
│ │ │ └── index.ts
│ ├── Node
│ │ ├── Node.default.tsx
│ │ ├── Node.wrapper.tsx
│ │ └── index.ts
│ ├── NodeInner
│ │ ├── NodeInner.default.tsx
│ │ └── index.ts
│ ├── Port
│ │ ├── Port.default.tsx
│ │ ├── Port.wrapper.tsx
│ │ └── index.ts
│ ├── Ports
│ │ ├── Ports.default.tsx
│ │ └── index.ts
│ ├── PortsGroup
│ │ ├── PortsGroup.default.tsx
│ │ └── index.ts
│ └── index.ts
├── constants.ts
├── container
│ ├── FlowChartWithState.tsx
│ ├── actions.ts
│ ├── index.ts
│ └── utils
│ │ ├── mapValues.ts
│ │ ├── rotate.ts
│ │ └── validateLink.ts
├── index.ts
├── types
│ ├── chart.ts
│ ├── config.ts
│ ├── functions.ts
│ ├── generics.ts
│ └── index.ts
└── utils.ts
├── stories
├── ConfigSnapToGrid.tsx
├── ConfigValidateLink.tsx
├── CustomCanvasOuter.tsx
├── CustomLink.tsx
├── CustomNode.tsx
├── CustomNodeInner.tsx
├── CustomPort.tsx
├── DragAndDropSidebar.tsx
├── ExternalReactState.tsx
├── InternalReactState.tsx
├── LinkColors.tsx
├── ReadonlyMode.tsx
├── SelectedSidebar.tsx
├── SmartRouting.tsx
├── StressTest.tsx
├── Zoom.tsx
├── components
│ ├── Content.tsx
│ ├── Page.tsx
│ ├── Sidebar.tsx
│ ├── SidebarItem.tsx
│ └── index.ts
├── index.tsx
├── misc
│ └── exampleChartState.ts
└── utils
│ └── throttleRender.tsx
├── tsconfig.json
├── tslint.json
└── typings.d.ts
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:8.11.3
6 | working_directory: ~/repo
7 |
8 | steps:
9 | - checkout
10 |
11 | - restore_cache:
12 | keys:
13 | - v1-dependencies-{{ checksum "package-lock.json" }}
14 | - v1-dependencies-
15 |
16 | - run: npm i
17 |
18 | - save_cache:
19 | paths:
20 | - node_modules
21 | key: v1-dependencies-{{ checksum "package-lock.json" }}
22 |
23 | - run: npm run lint
24 |
25 | - run: npm run build:storybook
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Storybook
9 | storybook-static
10 | build
11 | dist
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # TypeScript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 | # next.js build output
63 | .next
64 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/react-flow-chart.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 1600962472997
51 |
52 |
53 | 1600962472997
54 |
55 |
56 |
57 | 1600962616778
58 |
59 |
60 |
61 | 1600962616778
62 |
63 |
64 | 1600962779247
65 |
66 |
67 |
68 | 1600962779247
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased]
4 |
5 | ## [0.0.13] - 2020-05-09
6 |
7 | ## Fixed
8 |
9 | - Nodes is view calculation was wrong if the chart was zoomed leading to the nodes not displaying.
10 | - Moved `styled-components` to a peer dependency
11 | - Do not send click even when drag finishes [crsven](https://github.com/MrBlenny/react-flow-chart/pull/132)
12 |
13 | ## [0.0.12] - 2020-04-27
14 |
15 | ## Fixed
16 |
17 | - Fix a bad type annotation for `onLinkClick` [lukewarlow](https://github.com/MrBlenny/react-flow-chart/pull/107)
18 | - Pass config to `onCanvasDrop` [LeonZamel](https://github.com/MrBlenny/react-flow-chart/pull/111)
19 |
20 | ## Added
21 |
22 | - Use the data.id if it exists on the drag and drop data transfer object [NoyTse](https://github.com/MrBlenny/react-flow-chart/pull/96)
23 | - Add an onNodeDoubkeClick handler [jetmar](https://github.com/MrBlenny/react-flow-chart/pull/99)
24 | - Add properties.linkColor support to the default link component [ielijose](https://github.com/MrBlenny/react-flow-chart/pull/103)
25 | - Zoom support! [ielijose](https://github.com/MrBlenny/react-flow-chart/pull/125)
26 |
27 | ## Breaking
28 |
29 | - Readonly mode will no longer disable canvas drag [parasg1999](https://github.com/MrBlenny/react-flow-chart/pull/112)
30 | - Updated styled components to `^5.1.0` [ophirg](https://github.com/MrBlenny/react-flow-chart/pull/118)
31 | - Zoom is enabled by default
32 | - Chart state must have now have a scale property
33 |
34 | ## [0.0.11] - 2020-03-02
35 |
36 | ## Fixed
37 |
38 | - Fixed an issue with onDrag [errors](https://github.com/MrBlenny/react-flow-chart/pull/88#issuecomment-593213248)
39 |
40 | ## [0.0.10] - 2020-03-02
41 |
42 | ## Added
43 |
44 | - `smartRouting` mode [dmitrygalperin](https://github.com/MrBlenny/react-flow-chart/pull/89)
45 | - Pass node into ports to enable customisation [fenech](https://github.com/MrBlenny/react-flow-chart/pull/85)
46 | - Add `nodeMouseEnter` and `nodeMouseLeave` callbacks [fenech](https://github.com/MrBlenny/react-flow-chart/pull/84)
47 | - Add `onDragCanvasStop` and `onDragNodeStop` callbacks [lordi](https://github.com/MrBlenny/react-flow-chart/pull/88)
48 |
49 | ## [0.0.9] - 2020-01-18
50 |
51 | ## Fixed
52 |
53 | - The `onNodeClick` action will no longer be called when dragging [fenech](https://github.com/MrBlenny/react-flow-chart/pull/78/files)
54 | - Fix data consistency when `snapToGrid` is on/off [sinan](https://github.com/MrBlenny/react-flow-chart/pull/72)
55 | - Update node size when size changes in the DOM [zetavg](https://github.com/MrBlenny/react-flow-chart/pull/71)
56 | - Prevent links and ports displaying as active when in readonly mode.
57 |
58 | ## Breaking
59 |
60 | - Updated styled components to `^5.0.0` [yuyokk](https://github.com/MrBlenny/react-flow-chart/pull/76/files)
61 |
62 | ## [0.0.8] - 2019-10-20
63 |
64 | ## Fixed
65 |
66 | - Readonly mode should prevent link edits [loonyuni](https://github.com/MrBlenny/react-flow-chart/pull/45)
67 | - Only call `onCanvasDrop` if data exists in drop event [loonyuni](https://github.com/MrBlenny/react-flow-chart/pull/51)
68 | - Improve CustomNode storybook example [timbrunette](https://github.com/MrBlenny/react-flow-chart/pulls)
69 | - Fixed an error that was being thown when creating links in readonly mode
70 |
71 | ## [0.0.7] - 2019-08-22
72 |
73 | ## Added
74 |
75 | - Readonly mode [yukai-w](https://github.com/MrBlenny/react-flow-chart/pull/39)
76 | - Offset position to dropped item position [phlickey](https://github.com/MrBlenny/react-flow-chart/pull/34)
77 | - `snapToGrid` and `gridSize` config options [msteinmn](https://github.com/MrBlenny/react-flow-chart/pull/23)
78 | - `validateLink` config function [msteinmn](https://github.com/MrBlenny/react-flow-chart/pull/23)
79 | - misc other fixes [msteinmn](https://github.com/MrBlenny/react-flow-chart/pull/23)
80 | - Config object that is accessible by all components and actions
81 |
82 | ## Breaking
83 |
84 | - Changed the callback type signatures so they are always objects rather than functions with params. If you use custom callbacks, these will need to be updated.
85 |
86 |
87 | ## [0.0.6] - 2019-04-30
88 |
89 | ## Added
90 |
91 | - Upgrade Typescript and Storybook.
92 | - Prevent re-rendering for nodes and links that are not in use. [alexkuz PR7](https://github.com/MrBlenny/react-flow-chart/pull/7)
93 | - Render only nodes currently visible for user. [alexkuz PR7](https://github.com/MrBlenny/react-flow-chart/pull/7)
94 | - Fix calculating link position when canvas is not positioned in top left corner. [alexkuz PR7](https://github.com/MrBlenny/react-flow-chart/pull/7)
95 |
96 | ## Breaking
97 |
98 | - Added a new [onNodeSizeChange](https://github.com/MrBlenny/react-flow-chart/pull/7/files#diff-5a121158d13f502e78c5c29411f54269R141) action that is required for calculating which nodes are visible. If you are using external state, this will need to be implemented.
99 |
100 | ## [0.0.5] - 2019-04-02
101 |
102 | ### Added
103 |
104 | - Fixed a bug where links would not work on firefox [tantayou999](https://github.com/MrBlenny/react-flow-chart/issues/12)
105 |
106 | ## [0.0.4] - 2019-03-24
107 |
108 | ### Added
109 |
110 | - Start keeping a changelog
111 | - Remove storybook and lodash from from dist [alexcuz PR5](https://github.com/MrBlenny/react-flow-chart/pull/5)
112 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tortu C
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Flow Chart
2 |
3 |
4 | - [X] Dragabble Nodes and Canvas
5 | - [x] Create curved links between ports
6 | - [x] Custom components for Canvas, Links, Ports, Nodes
7 | - [X] React state container
8 | - [X] Update state on Select/Hover nodes, ports and links
9 | - [x] Base functionality complete
10 | - [X] Stable NPM version
11 | - [X] Scroll/Pinch canvas to zoom
12 | - [ ] Ctrl+z/Ctrl+y history
13 | - [X] Read-only mode
14 | - [ ] Redux state container
15 | - [ ] Arrow heads on links
16 | - [ ] Docs
17 |
18 |
19 | This project aims to build a highly customisable, declarative flow chart library. Critically, you control the state. Pick from Redux, MobX, React or any other state managment library - simply pass in the current state and hook up the callbacks.
20 |
21 | For example:
22 |
23 | 
24 |
25 | ## Data Stucture
26 |
27 | The flow chart is designed as a collection of Nodes, Ports and Links. You can specify your own custom properties, making this format quite flexible. See [types/chart.ts](./src/types/chart.ts). Note, nodes, ports and links should have a unique id.
28 |
29 | #### Example
30 |
31 | ```ts
32 |
33 | export const chart: IChart = {
34 | offset: {
35 | x: 0,
36 | y: 0,
37 | },
38 | scale: 1,
39 | nodes: {
40 | node1: {
41 | id: 'node1',
42 | type: 'output-only',
43 | position: {
44 | x: 300,
45 | y: 100,
46 | },
47 | ports: {
48 | port1: {
49 | id: 'port1',
50 | type: 'output',
51 | properties: {
52 | value: 'yes',
53 | },
54 | },
55 | port2: {
56 | id: 'port2',
57 | type: 'output',
58 | properties: {
59 | value: 'no',
60 | },
61 | },
62 | },
63 | },
64 | node2: {
65 | id: 'node2',
66 | type: 'input-output',
67 | position: {
68 | x: 300,
69 | y: 300,
70 | },
71 | ports: {
72 | port1: {
73 | id: 'port1',
74 | type: 'input',
75 | },
76 | port2: {
77 | id: 'port2',
78 | type: 'output',
79 | },
80 | },
81 | },
82 | },
83 | links: {
84 | link1: {
85 | id: 'link1',
86 | from: {
87 | nodeId: 'node1',
88 | portId: 'port2',
89 | },
90 | to: {
91 | nodeId: 'node2',
92 | portId: 'port1',
93 | },
94 | },
95 | },
96 | selected: {},
97 | hovered: {},
98 | }
99 |
100 | ```
101 |
102 | This will produce a simple 2 noded chart which looks like:
103 |
104 | 
105 |
106 | ## Basic Usage
107 |
108 | ```bash
109 | npm i @mrblenny/react-flow-chart
110 | ```
111 |
112 | Most components/types are available as a root level export. Check the storybook demo for more examples.
113 |
114 | ```tsx
115 | import { FlowChartWithState } from "@mrblenny/react-flow-chart";
116 |
117 | const chartSimple = {
118 | offset: {
119 | x: 0,
120 | y: 0
121 | },
122 | nodes: {
123 | node1: {
124 | id: "node1",
125 | type: "output-only",
126 | position: {
127 | x: 300,
128 | y: 100
129 | },
130 | ports: {
131 | port1: {
132 | id: "port1",
133 | type: "output",
134 | properties: {
135 | value: "yes"
136 | }
137 | },
138 | port2: {
139 | id: "port2",
140 | type: "output",
141 | properties: {
142 | value: "no"
143 | }
144 | }
145 | }
146 | },
147 | node2: {
148 | id: "node2",
149 | type: "input-output",
150 | position: {
151 | x: 300,
152 | y: 300
153 | },
154 | ports: {
155 | port1: {
156 | id: "port1",
157 | type: "input"
158 | },
159 | port2: {
160 | id: "port2",
161 | type: "output"
162 | }
163 | }
164 | },
165 | },
166 | links: {
167 | link1: {
168 | id: "link1",
169 | from: {
170 | nodeId: "node1",
171 | portId: "port2"
172 | },
173 | to: {
174 | nodeId: "node2",
175 | portId: "port1"
176 | },
177 | },
178 | },
179 | selected: {},
180 | hovered: {}
181 | };
182 |
183 | const Example = (
184 |
185 | );
186 | ```
187 |
188 | ## Contributing
189 |
190 | If you're interested in helping out, let me know.
191 |
192 | In particular, would be great to get a hand with docs and redux / mobx integrations.
193 |
194 |
195 | ## Development
196 |
197 | ```bash
198 | npm install
199 | npm run start:storybook
200 | ```
201 |
--------------------------------------------------------------------------------
/config/storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-options/register'
2 | import '@storybook/addon-viewport/register'
--------------------------------------------------------------------------------
/config/storybook/config.js:
--------------------------------------------------------------------------------
1 | import { setOptions } from "@storybook/addon-options"
2 | import { configure } from "@storybook/react"
3 |
4 | setOptions({
5 | hierarchySeparator: /\/|\./,
6 | hierarchyRootSeparator: /\|/,
7 | })
8 |
9 | function requireAll(requireContext) {
10 | return requireContext.keys().map(requireContext);
11 | }
12 |
13 | function loadStories() {
14 | requireAll(require.context("../../stories", true, /\.tsx?$/));
15 | }
16 |
17 | configure(loadStories, module);
--------------------------------------------------------------------------------
/config/storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ config }) => {
2 | config.module.rules.push({
3 | test: /\.(ts|tsx)$/,
4 | use: [
5 | {
6 | loader: require.resolve('ts-loader'),
7 | },
8 | ],
9 | });
10 | config.resolve.extensions.push('.ts', '.tsx');
11 | return config;
12 | };
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sailingdev/React-flow-chart/e33182d3725bab2b5ed25b0e51ea1e1e80498142/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/iframe.html:
--------------------------------------------------------------------------------
1 |
StorybookNo Preview
Sorry, but you either have no stories or none are selected somehow.
- Please check the Storybook config.
- Try reloading the page.
If the problem persists, check the browser console, or the terminal you've run Storybook from.
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | Storybook
--------------------------------------------------------------------------------
/docs/main.a04756a8b33b9f0f49ad.bundle.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{1542:function(n,o,c){"use strict";c.r(o);c(1543),c(1546)},470:function(n,o,c){c(471),c(854),n.exports=c(1542)},610:function(n,o){}},[[470,1,2]]]);
--------------------------------------------------------------------------------
/docs/main.a0afedec1e09075b22ed.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"main.a0afedec1e09075b22ed.bundle.js","sources":["webpack:///main.a0afedec1e09075b22ed.bundle.js"],"mappings":"AAAA","sourceRoot":""}
--------------------------------------------------------------------------------
/docs/runtime~main.a0afedec1e09075b22ed.bundle.js:
--------------------------------------------------------------------------------
1 | !function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i
40 | *
41 | * Copyright (c) 2014-2017, Jon Schlinkert.
42 | * Released under the MIT License.
43 | */
44 |
45 | /**
46 | * @license
47 | * Lodash
48 | * Copyright OpenJS Foundation and other contributors
49 | * Released under MIT license
50 | * Based on Underscore.js 1.8.3
51 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
52 | */
53 |
54 | /** @license React v0.18.0
55 | * scheduler.production.min.js
56 | *
57 | * Copyright (c) Facebook, Inc. and its affiliates.
58 | *
59 | * This source code is licensed under the MIT license found in the
60 | * LICENSE file in the root directory of this source tree.
61 | */
62 |
63 | /** @license React v16.12.0
64 | * react-dom.production.min.js
65 | *
66 | * Copyright (c) Facebook, Inc. and its affiliates.
67 | *
68 | * This source code is licensed under the MIT license found in the
69 | * LICENSE file in the root directory of this source tree.
70 | */
71 |
72 | /** @license React v16.12.0
73 | * react-is.production.min.js
74 | *
75 | * Copyright (c) Facebook, Inc. and its affiliates.
76 | *
77 | * This source code is licensed under the MIT license found in the
78 | * LICENSE file in the root directory of this source tree.
79 | */
80 |
81 | /** @license React v16.12.0
82 | * react.production.min.js
83 | *
84 | * Copyright (c) Facebook, Inc. and its affiliates.
85 | *
86 | * This source code is licensed under the MIT license found in the
87 | * LICENSE file in the root directory of this source tree.
88 | */
89 |
90 | /**!
91 | * @fileOverview Kickass library to create and place poppers near their reference elements.
92 | * @version 1.16.1
93 | * @license
94 | * Copyright (c) 2016 Federico Zivolo and contributors
95 | *
96 | * Permission is hereby granted, free of charge, to any person obtaining a copy
97 | * of this software and associated documentation files (the "Software"), to deal
98 | * in the Software without restriction, including without limitation the rights
99 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
100 | * copies of the Software, and to permit persons to whom the Software is
101 | * furnished to do so, subject to the following conditions:
102 | *
103 | * The above copyright notice and this permission notice shall be included in all
104 | * copies or substantial portions of the Software.
105 | *
106 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
107 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
108 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
109 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
110 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
111 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
112 | * SOFTWARE.
113 | */
114 |
--------------------------------------------------------------------------------
/docs/vendors~main.a0afedec1e09075b22ed.bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * https://github.com/es-shims/es5-shim
9 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License
10 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE
11 | */
12 |
13 | /*!
14 | * https://github.com/paulmillr/es6-shim
15 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com)
16 | * and contributors, MIT License
17 | * es6-shim: v0.35.4
18 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE
19 | * Details and documentation:
20 | * https://github.com/paulmillr/es6-shim/
21 | */
22 |
23 | /*!
24 | * is-plain-object
25 | *
26 | * Copyright (c) 2014-2017, Jon Schlinkert.
27 | * Released under the MIT License.
28 | */
29 |
30 | /*!
31 | * isobject
32 | *
33 | * Copyright (c) 2014-2017, Jon Schlinkert.
34 | * Released under the MIT License.
35 | */
36 |
37 | /**
38 | * @license
39 | * Lodash
40 | * Copyright OpenJS Foundation and other contributors
41 | * Released under MIT license
42 | * Based on Underscore.js 1.8.3
43 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
44 | */
45 |
46 | /** @license React v0.13.5
47 | * scheduler.production.min.js
48 | *
49 | * Copyright (c) Facebook, Inc. and its affiliates.
50 | *
51 | * This source code is licensed under the MIT license found in the
52 | * LICENSE file in the root directory of this source tree.
53 | */
54 |
55 | /** @license React v16.13.1
56 | * react-is.production.min.js
57 | *
58 | * Copyright (c) Facebook, Inc. and its affiliates.
59 | *
60 | * This source code is licensed under the MIT license found in the
61 | * LICENSE file in the root directory of this source tree.
62 | */
63 |
64 | /** @license React v16.8.4
65 | * react.production.min.js
66 | *
67 | * Copyright (c) Facebook, Inc. and its affiliates.
68 | *
69 | * This source code is licensed under the MIT license found in the
70 | * LICENSE file in the root directory of this source tree.
71 | */
72 |
73 | /** @license React v16.8.5
74 | * react-dom.production.min.js
75 | *
76 | * Copyright (c) Facebook, Inc. and its affiliates.
77 | *
78 | * This source code is licensed under the MIT license found in the
79 | * LICENSE file in the root directory of this source tree.
80 | */
81 |
82 | /** @license React v16.8.6
83 | * react-is.production.min.js
84 | *
85 | * Copyright (c) Facebook, Inc. and its affiliates.
86 | *
87 | * This source code is licensed under the MIT license found in the
88 | * LICENSE file in the root directory of this source tree.
89 | */
90 |
91 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable
92 |
93 | //! © 2018 Angry Bytes and contributors. MIT licensed.
94 |
--------------------------------------------------------------------------------
/docs/vendors~main.a0afedec1e09075b22ed.bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"vendors~main.a0afedec1e09075b22ed.bundle.js","sources":["webpack:///vendors~main.a0afedec1e09075b22ed.bundle.js"],"mappings":";AAAA","sourceRoot":""}
--------------------------------------------------------------------------------
/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sailingdev/React-flow-chart/e33182d3725bab2b5ed25b0e51ea1e1e80498142/images/demo.gif
--------------------------------------------------------------------------------
/images/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sailingdev/React-flow-chart/e33182d3725bab2b5ed25b0e51ea1e1e80498142/images/demo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mrblenny/react-flow-chart",
3 | "version": "0.0.13",
4 | "description": "A flexible, stateless flow chart library for react.",
5 | "main": "src/index.js",
6 | "repository": "git@github.com:MrBlenny/react-flow-chart.git",
7 | "author": "David Revay ",
8 | "license": "MIT",
9 | "devDependencies": {
10 | "@babel/core": "^7.1.2",
11 | "@storybook/addon-centered": "^5.3.18",
12 | "@storybook/addon-options": "5.3.18",
13 | "@storybook/addon-viewport": "5.3.18",
14 | "@storybook/react": "5.3.18",
15 | "@types/lodash": "^4.14.118",
16 | "@types/node": "10.12.0",
17 | "@types/pathfinding": "0.0.4",
18 | "@types/react": "^16.8.8",
19 | "@types/react-dom": "^16.8.4",
20 | "@types/styled-components": "^4.1.19",
21 | "@types/uuid": "^3.4.4",
22 | "@types/webpack": "4.4.17",
23 | "babel-loader": "^8.0.4",
24 | "lodash": "^4.17.15",
25 | "np": "^3.0.4",
26 | "react": "^16.8.4",
27 | "react-dom": "^16.8.5",
28 | "react-json-view": "^1.19.1",
29 | "styled-components": "^5.1.0",
30 | "ts-loader": "^5.2.2",
31 | "ts-node": "^7.0.1",
32 | "tslint": "^5.11.0",
33 | "tslint-config-standard": "^8.0.1",
34 | "tslint-react": "^3.6.0",
35 | "typescript": "^3.4.1",
36 | "webpack": "^4.21.0",
37 | "webpack-cli": "^3.3.11"
38 | },
39 | "dependencies": {
40 | "pathfinding": "^0.4.18",
41 | "react-draggable": "^4.3.1",
42 | "react-resize-observer": "^1.1.1",
43 | "react-zoom-pan-pinch": "^1.6.1",
44 | "uuid": "^3.3.2"
45 | },
46 | "peerDependencies": {
47 | "react": "^16.8.4",
48 | "react-dom": "^16.8.4",
49 | "styled-components": "^5.1.0"
50 | },
51 | "scripts": {
52 | "start:storybook": "start-storybook -p 6006 -c config/storybook",
53 | "build:storybook": "build-storybook -c config/storybook -o docs",
54 | "build": "tsc && cp package.json dist/ && cp README.md ./dist",
55 | "test": "npm run lint",
56 | "lint": "tslint -p tsconfig.json -c tslint.json"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Canvas/Canvas.wrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'
3 | import { IConfig, IOnCanvasClick, IOnCanvasDrop, IOnDeleteKey, IOnDragCanvas, IOnDragCanvasStop, IOnZoomCanvas, REACT_FLOW_CHART } from '../../'
4 | import CanvasContext from './CanvasContext'
5 | import { ICanvasInnerDefaultProps } from './CanvasInner.default'
6 | import { ICanvasOuterDefaultProps } from './CanvasOuter.default'
7 |
8 | export interface ICanvasWrapperProps {
9 | config: IConfig
10 | position: {
11 | x: number
12 | y: number,
13 | }
14 | scale: number
15 | onZoomCanvas: IOnZoomCanvas
16 | onDragCanvas: IOnDragCanvas
17 | onDragCanvasStop: IOnDragCanvasStop
18 | onDeleteKey: IOnDeleteKey
19 | onCanvasClick: IOnCanvasClick
20 | onCanvasDrop: IOnCanvasDrop
21 | ComponentInner: React.FunctionComponent
22 | ComponentOuter: React.FunctionComponent
23 | onSizeChange: (x: number, y: number) => void
24 | children: any
25 | }
26 |
27 | interface IState {
28 | width: number
29 | height: number
30 | offsetX: number
31 | offsetY: number
32 | }
33 |
34 | export class CanvasWrapper extends React.Component {
35 | public state = {
36 | width: 0,
37 | height: 0,
38 | offsetX: 0,
39 | offsetY: 0,
40 | }
41 |
42 | private ref = React.createRef()
43 |
44 | public componentDidMount () {
45 | this.updateSize()
46 |
47 | if (this.ref.current) {
48 | if ((window as any).ResizeObserver) {
49 | const ro = new (window as any).ResizeObserver(this.updateSize)
50 | ro.observe(this.ref.current)
51 | } else {
52 | window.addEventListener('resize', this.updateSize)
53 | }
54 | window.addEventListener('scroll', this.updateSize)
55 | }
56 | }
57 |
58 | public componentDidUpdate () {
59 | this.updateSize()
60 | }
61 |
62 | public componentWillUnmount () {
63 | window.removeEventListener('resize', this.updateSize)
64 | window.removeEventListener('scroll', this.updateSize)
65 | }
66 |
67 | public render () {
68 | const {
69 | config,
70 | scale,
71 | ComponentInner,
72 | ComponentOuter,
73 | position,
74 | onDragCanvas,
75 | onDragCanvasStop,
76 | children,
77 | onCanvasClick,
78 | onDeleteKey,
79 | onCanvasDrop,
80 | onZoomCanvas,
81 | } = this.props
82 | const { offsetX, offsetY } = this.state
83 | const { zoom } = config
84 |
85 | const options = {
86 | transformEnabled: zoom && zoom.transformEnabled ? zoom.transformEnabled : true,
87 | minScale: zoom && zoom.minScale ? zoom.minScale : 0.25,
88 | maxScale: zoom && zoom.maxScale ? zoom.maxScale : 2,
89 | limitToBounds: false,
90 | limitToWrapper: false,
91 | centerContent: false,
92 | }
93 |
94 | const doubleClickMode = config.readonly ? 'zoomOut' : 'zoomIn'
95 |
96 | return (
97 |
104 |
105 | onZoomCanvas({ config, data })}
119 | onWheelStop={(data: any) => onZoomCanvas({ config, data })}
120 | onPanning={(data: any) => onDragCanvas({ config, data })}
121 | onPanningStop={(data: any) => onDragCanvasStop({ config, data })}
122 | >
123 |
124 | {
130 | // delete or backspace keys
131 | if (e.keyCode === 46 || e.keyCode === 8) {
132 | onDeleteKey({ config })
133 | }
134 | }}
135 | onDrop={(e) => {
136 | const data = JSON.parse(
137 | e.dataTransfer.getData(REACT_FLOW_CHART),
138 | )
139 | if (data) {
140 | onCanvasDrop({
141 | config,
142 | data,
143 | position: {
144 | x: e.clientX - (position.x + offsetX),
145 | y: e.clientY - (position.y + offsetY),
146 | },
147 | })
148 | }
149 | }}
150 | onDragOver={(e) => e.preventDefault()}
151 | />
152 |
153 |
154 |
155 |
156 | )
157 | }
158 |
159 | private updateSize = () => {
160 | const el = this.ref.current
161 |
162 | if (el) {
163 | const rect = el.getBoundingClientRect()
164 |
165 | if (el.offsetWidth !== this.state.width || el.offsetHeight !== this.state.height) {
166 | this.setState({ width: el.offsetWidth, height: el.offsetHeight })
167 | this.props.onSizeChange(el.offsetWidth, el.offsetHeight)
168 | }
169 |
170 | if (rect.left !== this.state.offsetX || rect.top !== this.state.offsetY) {
171 | this.setState({ offsetX: rect.left, offsetY: rect.top })
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/components/Canvas/CanvasContext.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // NB: always import CanvasContext directly from this file to prevent circular module imports
4 | // see https://github.com/facebook/react/issues/13969#issuecomment-433253469
5 |
6 | const CanvasContext = React.createContext({ offsetX: 0, offsetY: 0, zoomScale: 1 })
7 |
8 | export default CanvasContext
9 |
--------------------------------------------------------------------------------
/src/components/Canvas/CanvasInner.default.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { IConfig, IOnCanvasClick } from '../../'
3 |
4 | export interface ICanvasInnerDefaultProps {
5 | config: IConfig
6 | children: any
7 | onClick: IOnCanvasClick
8 | tabIndex: number
9 | onKeyDown: (e: React.KeyboardEvent) => void
10 | onDrop: (e: React.DragEvent) => void
11 | onDragOver: (e: React.DragEvent) => void
12 | }
13 |
14 | export const CanvasInnerDefault = styled.div`
15 | position: relative;
16 | outline: 1px dashed rgba(0,0,0,0.1);
17 | width: 10000px;
18 | height: 10000px;
19 | cursor: move;
20 | ` as any
21 |
--------------------------------------------------------------------------------
/src/components/Canvas/CanvasOuter.default.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { IConfig } from '../../types'
3 |
4 | export interface ICanvasOuterDefaultProps {
5 | config: IConfig
6 | children: any
7 | ref?: React.Ref
8 | }
9 |
10 | export const CanvasOuterDefault = styled.div`
11 | position: relative;
12 | background-size: 20px 20px;
13 | background-color: rgba(0,0,0,0.08);
14 | background-image:
15 | linear-gradient(90deg,hsla(0,0%,100%,.2) 1px,transparent 0),
16 | linear-gradient(180deg,hsla(0,0%,100%,.2) 1px,transparent 0);
17 | width: 100%;
18 | overflow: hidden;
19 | cursor: not-allowed;
20 | ` as any
21 |
--------------------------------------------------------------------------------
/src/components/Canvas/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './CanvasInner.default'
2 | export * from './CanvasOuter.default'
3 | export * from './Canvas.wrapper'
4 |
--------------------------------------------------------------------------------
/src/components/FlowChart/FlowChart.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | CanvasInnerDefault, CanvasOuterDefault, CanvasWrapper, ICanvasInnerDefaultProps, ICanvasOuterDefaultProps, IChart, IConfig, ILink,
4 | ILinkDefaultProps, INodeDefaultProps, INodeInnerDefaultProps, IOnCanvasClick, IOnCanvasDrop, IOnDeleteKey, IOnDragCanvas,
5 | IOnDragCanvasStop, IOnDragNode, IOnDragNodeStop, IOnLinkCancel, IOnLinkClick, IOnLinkComplete, IOnLinkMouseEnter,
6 | IOnLinkMouseLeave, IOnLinkMove, IOnLinkStart, IOnNodeClick, IOnNodeDoubleClick, IOnNodeMouseEnter, IOnNodeMouseLeave, IOnNodeSizeChange,
7 | IOnPortPositionChange, IOnZoomCanvas, IPortDefaultProps, IPortsDefaultProps, ISelectedOrHovered, LinkDefault, LinkWrapper, NodeDefault, NodeInnerDefault, NodeWrapper, PortDefault, PortsDefault,
8 | } from '../../'
9 | import { getMatrix } from './utils/grid'
10 |
11 | export interface IFlowChartCallbacks {
12 | onDragNode: IOnDragNode
13 | onDragNodeStop: IOnDragNodeStop
14 | onDragCanvas: IOnDragCanvas
15 | onCanvasDrop: IOnCanvasDrop
16 | onDragCanvasStop: IOnDragCanvasStop
17 | onLinkStart: IOnLinkStart
18 | onLinkMove: IOnLinkMove
19 | onLinkComplete: IOnLinkComplete
20 | onLinkCancel: IOnLinkCancel
21 | onPortPositionChange: IOnPortPositionChange
22 | onLinkMouseEnter: IOnLinkMouseEnter
23 | onLinkMouseLeave: IOnLinkMouseLeave
24 | onLinkClick: IOnLinkClick
25 | onCanvasClick: IOnCanvasClick
26 | onDeleteKey: IOnDeleteKey
27 | onNodeClick: IOnNodeClick
28 | onNodeDoubleClick: IOnNodeDoubleClick
29 | onNodeMouseEnter: IOnNodeMouseEnter
30 | onNodeMouseLeave: IOnNodeMouseLeave
31 | onNodeSizeChange: IOnNodeSizeChange
32 | onZoomCanvas: IOnZoomCanvas
33 | }
34 |
35 | export interface IFlowChartComponents {
36 | CanvasOuter?: React.FunctionComponent
37 | CanvasInner?: React.FunctionComponent
38 | NodeInner?: React.FunctionComponent
39 | Ports?: React.FunctionComponent
40 | Port?: React.FunctionComponent
41 | Node?: React.FunctionComponent
42 | Link?: React.FunctionComponent
43 | }
44 |
45 | export interface IFlowChartProps {
46 | /**
47 | * The current chart state
48 | */
49 | chart: IChart
50 | /**
51 | * Callbacks for updating chart state.
52 | * See container/actions.ts for example state mutations
53 | */
54 | callbacks: IFlowChartCallbacks
55 | /**
56 | * Custom components
57 | */
58 | Components?: IFlowChartComponents
59 | /**
60 | * Other config. This will be passed into all components and actions.
61 | * Don't store state here as it may trigger re-renders
62 | */
63 | config?: IConfig
64 | }
65 |
66 | export const FlowChart = (props: IFlowChartProps) => {
67 | const [ canvasSize, setCanvasSize ] = React.useState<{ width: number, height: number }>({ width: 0, height: 0 })
68 |
69 | const {
70 | chart,
71 | callbacks: {
72 | onDragNode,
73 | onDragNodeStop,
74 | onDragCanvas,
75 | onDragCanvasStop,
76 | onCanvasDrop,
77 | onLinkStart,
78 | onLinkMove,
79 | onLinkComplete,
80 | onLinkCancel,
81 | onPortPositionChange,
82 | onLinkMouseEnter,
83 | onLinkMouseLeave,
84 | onLinkClick,
85 | onCanvasClick,
86 | onDeleteKey,
87 | onNodeClick,
88 | onNodeDoubleClick,
89 | onNodeMouseEnter,
90 | onNodeMouseLeave,
91 | onNodeSizeChange,
92 | onZoomCanvas,
93 | },
94 | Components: {
95 | CanvasOuter = CanvasOuterDefault,
96 | CanvasInner = CanvasInnerDefault,
97 | NodeInner = NodeInnerDefault,
98 | Ports = PortsDefault,
99 | Port = PortDefault,
100 | Node = NodeDefault,
101 | Link = LinkDefault,
102 | } = {},
103 | config = {},
104 | } = props
105 | const { links, nodes, selected, hovered, offset, scale } = chart
106 |
107 | const canvasCallbacks = { onDragCanvas, onDragCanvasStop, onCanvasClick, onDeleteKey, onCanvasDrop, onZoomCanvas }
108 | const linkCallbacks = { onLinkMouseEnter, onLinkMouseLeave, onLinkClick }
109 | const nodeCallbacks = { onDragNode, onNodeClick, onDragNodeStop, onNodeMouseEnter, onNodeMouseLeave, onNodeSizeChange,onNodeDoubleClick }
110 | const portCallbacks = { onPortPositionChange, onLinkStart, onLinkMove, onLinkComplete, onLinkCancel }
111 |
112 | const nodesInView = Object.keys(nodes).filter((nodeId) => {
113 | const defaultNodeSize = { width: 500, height: 500 }
114 |
115 | const { x, y } = nodes[nodeId].position
116 | const size = nodes[nodeId].size || defaultNodeSize
117 |
118 | const isTooFarLeft = scale * x + offset.x + scale * size.width < 0
119 | const isTooFarRight = scale * x + offset.x > canvasSize.width
120 | const isTooFarUp = scale * y + offset.y + scale * size.height < 0
121 | const isTooFarDown = scale * y + offset.y > canvasSize.height
122 | return !(isTooFarLeft || isTooFarRight || isTooFarUp || isTooFarDown)
123 | })
124 |
125 | const matrix = config.smartRouting ? getMatrix(chart.offset, Object.values(nodesInView.map((nodeId) => nodes[nodeId]))) : undefined
126 |
127 | const linksInView = Object.keys(links).filter((linkId) => {
128 | const from = links[linkId].from
129 | const to = links[linkId].to
130 |
131 | return (
132 | !to.nodeId ||
133 | nodesInView.indexOf(from.nodeId) !== -1 ||
134 | nodesInView.indexOf(to.nodeId) !== -1
135 | )
136 | })
137 |
138 | return (
139 | setCanvasSize({ width, height })}
146 | {...canvasCallbacks}
147 | >
148 | { linksInView.map((linkId) => {
149 | const isSelected = !config.readonly && selected.type === 'link' && selected.id === linkId
150 | const isHovered = !config.readonly && hovered.type === 'link' && hovered.id === linkId
151 | const fromNodeId = links[linkId].from.nodeId
152 | const toNodeId = links[linkId].to.nodeId
153 |
154 | return (
155 |
167 | )
168 | })}
169 | { nodesInView.map((nodeId) => {
170 | const isSelected = selected.type === 'node' && selected.id === nodeId
171 | const selectedLink = getSelectedLinkForNode(selected, nodeId, links)
172 | const hoveredLink = getSelectedLinkForNode(hovered, nodeId, links)
173 |
174 | return (
175 |
192 | )
193 | })
194 | }
195 |
196 | )
197 | }
198 |
199 | const getSelectedLinkForNode = (
200 | selected: ISelectedOrHovered,
201 | nodeId: string,
202 | links: IChart['links'],
203 | ): ILink | undefined => {
204 | const link = selected.type === 'link' && selected.id ? links[selected.id] : undefined
205 |
206 | if (link && (link.from.nodeId === nodeId || link.to.nodeId === nodeId)) {
207 | return link
208 | }
209 |
210 | return undefined
211 | }
212 |
--------------------------------------------------------------------------------
/src/components/FlowChart/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FlowChart'
2 |
--------------------------------------------------------------------------------
/src/components/FlowChart/utils/grid.ts:
--------------------------------------------------------------------------------
1 | const SCALE_FACTOR = 5
2 | export const MATRIX_PADDING = 5
3 | export const NODE_PADDING = 3
4 |
5 | const getEmptyMatrix = (width: number, height: number): number[][] => {
6 |
7 | const adjustedWidth = Math.ceil(width / (SCALE_FACTOR - 1)) + MATRIX_PADDING
8 | const adjustedHeight = Math.ceil(height / (SCALE_FACTOR - 1)) + MATRIX_PADDING
9 |
10 | const matrix = []
11 |
12 | for (let i = 0; i < adjustedHeight; i++) {
13 | matrix.push(new Array(adjustedWidth).fill(0))
14 | }
15 |
16 | return matrix
17 | }
18 |
19 | const getMatrixDimensions = (offset: { x: number, y: number }, nodeDimensions: any[]): { width: number, height: number } => {
20 | const defaultNodeSize = { width: 500, height: 500 }
21 | const dimensions = { width: 0, height: 0 }
22 |
23 | const offsetX = Math.max(offset.x, 0)
24 | const offsetY = Math.max(offset.y, 0)
25 |
26 | nodeDimensions.forEach((node) => {
27 |
28 | const size = node.size || defaultNodeSize
29 |
30 | const x = node.position.x + offsetX + size.width
31 | const y = node.position.y + offsetY + size.height
32 |
33 | if (x > dimensions.width) dimensions.width = x
34 | if (y > dimensions.height) dimensions.height = y
35 |
36 | })
37 |
38 | return dimensions
39 | }
40 |
41 | export const getMatrix = (offset: { x: number, y: number }, nodeDimensions: any[]): number[][] => {
42 | const { width, height } = getMatrixDimensions(offset, nodeDimensions)
43 | const matrix = getEmptyMatrix(width, height)
44 |
45 | nodeDimensions.forEach((dimension) => {
46 | const { position } = dimension
47 | const defaultNodeSize = { width: 500, height: 500 }
48 | const size = dimension.size || defaultNodeSize
49 |
50 | const scaledSize = {
51 | width: Math.ceil(size.width / SCALE_FACTOR) + NODE_PADDING,
52 | height: Math.ceil(size.height / SCALE_FACTOR) + NODE_PADDING,
53 | }
54 |
55 | const scaledX = Math.ceil(position.x / SCALE_FACTOR)
56 | const scaledY = Math.ceil(position.y / SCALE_FACTOR)
57 |
58 | for (let x = Math.max(scaledX - NODE_PADDING, 0); x <= scaledX + scaledSize.width; x++) {
59 | for (let y = Math.max(scaledY - NODE_PADDING, 0); y <= scaledY + scaledSize.height; y++) {
60 | matrix[y][x] = 1
61 | }
62 | }
63 | })
64 |
65 | return matrix
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Link/Link.default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { generateCurvePath, generateRightAnglePath, generateSmartPath, IConfig, ILink, IOnLinkClick, IOnLinkMouseEnter, IOnLinkMouseLeave, IPort, IPosition } from '../../'
3 |
4 | export interface ILinkDefaultProps {
5 | config: IConfig
6 | link: ILink
7 | startPos: IPosition
8 | endPos: IPosition
9 | fromPort: IPort
10 | toPort?: IPort
11 | onLinkMouseEnter: IOnLinkMouseEnter
12 | onLinkMouseLeave: IOnLinkMouseLeave
13 | onLinkClick: IOnLinkClick
14 | isHovered: boolean
15 | isSelected: boolean
16 | matrix?: number[][]
17 | }
18 |
19 | export const LinkDefault = ({
20 | config,
21 | link,
22 | startPos,
23 | endPos,
24 | fromPort,
25 | toPort,
26 | onLinkMouseEnter,
27 | onLinkMouseLeave,
28 | onLinkClick,
29 | isHovered,
30 | isSelected,
31 | matrix,
32 | }: ILinkDefaultProps) => {
33 |
34 | const points = config.smartRouting ?
35 | !!toPort && !!matrix ? generateSmartPath(matrix, startPos, endPos, fromPort, toPort) : generateRightAnglePath(startPos, endPos)
36 | : generateCurvePath(startPos, endPos)
37 |
38 | const linkColor: string = (fromPort.properties && fromPort.properties.linkColor) || 'cornflowerblue'
39 |
40 | return (
41 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/Link/Link.wrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { IConfig, ILink, INode, IOnLinkClick, IOnLinkMouseEnter, IOnLinkMouseLeave } from '../../'
3 | import { noop } from '../../utils'
4 | import { ILinkDefaultProps, LinkDefault } from './Link.default'
5 | import { getLinkPosition } from './utils'
6 |
7 | export interface ILinkWrapperProps {
8 | config: IConfig,
9 | link: ILink
10 | isSelected: boolean
11 | isHovered: boolean
12 | fromNode: INode
13 | toNode: INode | undefined
14 | onLinkMouseEnter: IOnLinkMouseEnter
15 | onLinkMouseLeave: IOnLinkMouseLeave
16 | onLinkClick: IOnLinkClick
17 | Component?: React.FunctionComponent
18 | matrix?: number[][]
19 | }
20 |
21 | export const LinkWrapper = React.memo(({
22 | config,
23 | Component = LinkDefault,
24 | link,
25 | onLinkMouseEnter,
26 | onLinkMouseLeave,
27 | onLinkClick,
28 | isSelected,
29 | isHovered,
30 | fromNode,
31 | toNode,
32 | matrix,
33 | }: ILinkWrapperProps) => {
34 | const startPos = getLinkPosition(fromNode, link.from.portId)
35 | const fromPort = fromNode.ports[link.from.portId]
36 |
37 | const endPos = toNode && link.to.portId
38 | ? getLinkPosition(toNode, link.to.portId)
39 | : link.to.position
40 | const toPort = toNode && link.to.portId ? toNode.ports[link.to.portId] : undefined
41 |
42 | // Don't render the link yet if there is no end pos
43 | // This will occur if the link was just created
44 | if (!endPos) {
45 | return null
46 | }
47 |
48 | return (
49 |
63 | )
64 | })
65 |
--------------------------------------------------------------------------------
/src/components/Link/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Link.default'
2 | export * from './Link.wrapper'
3 | export * from './utils'
4 |
--------------------------------------------------------------------------------
/src/components/Link/utils/generateCurvePath.ts:
--------------------------------------------------------------------------------
1 | import * as PF from 'pathfinding'
2 | import { IPort, IPosition } from '../../../'
3 | import { MATRIX_PADDING } from '../../FlowChart/utils/grid'
4 |
5 | export const generateCurvePath = (startPos: IPosition, endPos: IPosition): string => {
6 | const width = Math.abs(startPos.x - endPos.x)
7 | const height = Math.abs(startPos.y - endPos.y)
8 | const leftToRight = startPos.x < endPos.x
9 | const topToBottom = startPos.y < endPos.y
10 | const isHorizontal = width > height
11 |
12 | let start: IPosition
13 | let end: IPosition
14 | if (isHorizontal) {
15 | start = leftToRight ? startPos : endPos
16 | end = leftToRight ? endPos : startPos
17 | } else {
18 | start = topToBottom ? startPos : endPos
19 | end = topToBottom ? endPos : startPos
20 | }
21 |
22 | const curve = isHorizontal ? width / 3 : height / 3
23 | const curveX = isHorizontal ? curve : 0
24 | const curveY = isHorizontal ? 0 : curve
25 |
26 | return `M${start.x},${start.y} C ${start.x + curveX},${start.y + curveY} ${end.x - curveX},${end.y - curveY} ${end.x},${end.y}`
27 | }
28 |
29 | const finder = PF.JumpPointFinder({
30 | heuristic: PF.Heuristic.manhattan,
31 | diagonalMovement: PF.DiagonalMovement.Never,
32 | })
33 |
34 | export const generateRightAnglePath = (startPos: IPosition, endPos: IPosition) => {
35 | const width = Math.abs(startPos.x - endPos.x)
36 | const height = Math.abs(startPos.y - endPos.y)
37 | const leftToRight = startPos.x < endPos.x
38 | const topToBottom = startPos.y < endPos.y
39 | const isHorizontal = width > height
40 |
41 | let start: IPosition
42 | let end: IPosition
43 | if (isHorizontal) {
44 | start = leftToRight ? startPos : endPos
45 | end = leftToRight ? endPos : startPos
46 | } else {
47 | start = topToBottom ? startPos : endPos
48 | end = topToBottom ? endPos : startPos
49 | }
50 |
51 | const vertex = isHorizontal ? `${start.x},${end.y}` : `${end.x},${start.y}`
52 |
53 | return `M${start.x},${start.y} L ${vertex} ${end.x},${end.y}`
54 | }
55 |
56 | const setWalkableAtPorts = (start: { pos: IPosition, port: IPort }, end: { pos: IPosition, port: IPort }, grid: PF.Grid) => {
57 | ([start, end]).forEach((point) => {
58 | if (['input', 'top'].includes(point.port.type)) {
59 | for (let i = point.pos.y; i >= Math.max(point.pos.y - MATRIX_PADDING, 0); i--) {
60 | grid.setWalkableAt(point.pos.x, i, true)
61 | }
62 | } else if (['output', 'bottom'].includes(point.port.type)) {
63 | for (let i = point.pos.y; i <= Math.min(point.pos.y + MATRIX_PADDING, grid.height); i++) {
64 | grid.setWalkableAt(point.pos.x, i, true)
65 | }
66 | } else if (['right'].includes(point.port.type)) {
67 | for (let i = point.pos.x; i <= Math.max(point.pos.x + MATRIX_PADDING, grid.width); i++) {
68 | grid.setWalkableAt(i, point.pos.y, true)
69 | }
70 | } else if (['left'].includes(point.port.type)) {
71 | for (let i = point.pos.x; i >= Math.max(point.pos.x - MATRIX_PADDING, 0); i--) {
72 | grid.setWalkableAt(i, point.pos.y, true)
73 | }
74 | }
75 | })
76 | }
77 |
78 | export const generateSmartPath = (matrix: number[][], startPos: IPosition, endPos: IPosition, fromPort: IPort, toPort: IPort): string => {
79 | const grid = new PF.Grid(matrix)
80 |
81 | const startPosScaled = { x: Math.ceil(startPos.x / 5), y: Math.ceil(startPos.y / 5) }
82 | const endPosScaled = { x: Math.ceil(endPos.x / 5), y: Math.ceil(endPos.y / 5) }
83 |
84 | try {
85 | // try to find a smart path. use right angle path as a fallback
86 | setWalkableAtPorts({ pos : startPosScaled, port: fromPort }, { pos : endPosScaled, port: toPort }, grid)
87 |
88 | const path = PF.Util.compressPath(
89 | finder.findPath(
90 | startPosScaled.x,
91 | startPosScaled.y,
92 | endPosScaled.x,
93 | endPosScaled.y,
94 | grid,
95 | ),
96 | )
97 |
98 | if (!path.length) return generateRightAnglePath(startPos, endPos)
99 | const [first, ...rest] = path
100 | let d = `M${first[0] * 5} ${first[1] * 5}`
101 | rest.forEach(([x, y]) => {
102 | d += ` L${x * 5} ${y * 5}`
103 | })
104 | return d
105 | } catch (e) {
106 | return generateRightAnglePath(startPos, endPos)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/Link/utils/getLinkPosition.ts:
--------------------------------------------------------------------------------
1 | import { INode, IPosition } from '../../../'
2 |
3 | export const getLinkPosition = (node: INode, portId: string): IPosition => {
4 | const port = node.ports[portId]
5 | return {
6 | x: node.position.x + (port.position ? port.position.x : 0),
7 | y: node.position.y + (port.position ? port.position.y : 0),
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Link/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getLinkPosition'
2 | export * from './generateCurvePath'
3 |
--------------------------------------------------------------------------------
/src/components/Node/Node.default.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { IConfig, INode } from '../../'
3 |
4 | export interface INodeDefaultProps {
5 | config: IConfig
6 | node: INode
7 | children: any
8 | isSelected: boolean
9 | onClick: (e: React.MouseEvent) => void
10 | onDoubleClick: (e: React.MouseEvent) => void
11 | onMouseEnter: (e: React.MouseEvent) => void
12 | onMouseLeave: (e: React.MouseEvent) => void
13 | style?: object
14 | ref?: React.Ref
15 | }
16 |
17 | export const NodeDefault = styled.div`
18 | position: absolute;
19 | transition: 0.3s ease box-shadow, 0.3s ease margin-top;
20 | background: white;
21 | border-radius: 4px;
22 | min-width: 200px;
23 | ${(props) => props.isSelected && css`
24 | box-shadow: 0 10px 20px rgba(0,0,0,.1);
25 | margin-top: -2px
26 | `
27 | }
28 | ` as any
29 |
--------------------------------------------------------------------------------
/src/components/Node/Node.wrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import Draggable, { DraggableData } from 'react-draggable'
4 | import ResizeObserver from 'react-resize-observer'
5 | import {
6 | IConfig, ILink, INode, INodeInnerDefaultProps, IOnDragNode,
7 | IOnDragNodeStop, IOnLinkCancel,
8 | IOnLinkComplete, IOnLinkMove,
9 | IOnLinkStart, IOnNodeClick, IOnNodeDoubleClick, IOnNodeMouseEnter,
10 | IOnNodeMouseLeave, IOnNodeSizeChange, IOnPortPositionChange,
11 | IPortDefaultProps, IPortsDefaultProps,
12 | IPosition, ISelectedOrHovered, ISize, PortWrapper,
13 | } from '../../'
14 | import { noop } from '../../utils'
15 | import { INodeDefaultProps, NodeDefault } from './Node.default'
16 |
17 | export interface INodeWrapperProps {
18 | config: IConfig
19 | node: INode
20 | Component: React.FunctionComponent
21 | offset: IPosition
22 | selected: ISelectedOrHovered | undefined
23 | hovered: ISelectedOrHovered | undefined
24 | selectedLink: ILink | undefined
25 | hoveredLink: ILink | undefined
26 | isSelected: boolean
27 | NodeInner: React.FunctionComponent
28 | Ports: React.FunctionComponent
29 | Port: React.FunctionComponent
30 | onPortPositionChange: IOnPortPositionChange
31 | onLinkStart: IOnLinkStart
32 | onLinkMove: IOnLinkMove
33 | onLinkComplete: IOnLinkComplete
34 | onLinkCancel: IOnLinkCancel
35 | onDragNode: IOnDragNode
36 | onDragNodeStop: IOnDragNodeStop
37 | onNodeClick: IOnNodeClick
38 | onNodeDoubleClick: IOnNodeDoubleClick
39 | onNodeSizeChange: IOnNodeSizeChange
40 | onNodeMouseEnter: IOnNodeMouseEnter
41 | onNodeMouseLeave: IOnNodeMouseLeave
42 | }
43 |
44 | export const NodeWrapper = ({
45 | config,
46 | node,
47 | onDragNode,
48 | onDragNodeStop,
49 | onNodeClick,
50 | onNodeDoubleClick,
51 | isSelected,
52 | Component = NodeDefault,
53 | onNodeSizeChange,
54 | onNodeMouseEnter,
55 | onNodeMouseLeave,
56 | NodeInner,
57 | Ports,
58 | Port,
59 | offset,
60 | selected,
61 | selectedLink,
62 | hovered,
63 | hoveredLink,
64 | onPortPositionChange,
65 | onLinkStart,
66 | onLinkMove,
67 | onLinkComplete,
68 | onLinkCancel,
69 | }: INodeWrapperProps) => {
70 | const [size, setSize] = React.useState({ width: 0, height: 0 })
71 |
72 | const isDragging = React.useRef(false)
73 |
74 | const onStart = React.useCallback((e: MouseEvent) => {
75 | // Stop propagation so the canvas does not move
76 | e.stopPropagation()
77 | isDragging.current = false
78 | },[])
79 |
80 | const onDrag = React.useCallback((event: MouseEvent, data: DraggableData) => {
81 | isDragging.current = true
82 | onDragNode({ config, event, data, id: node.id })
83 | }, [onDragNode, config, node.id])
84 |
85 | const onStop = React.useCallback((event: MouseEvent, data: DraggableData) => {
86 | onDragNodeStop({ config, event, data, id: node.id })
87 | }, [onDragNodeStop, config, node.id])
88 |
89 | const onClick = React.useCallback((e: React.MouseEvent) => {
90 | if (!config.readonly) {
91 | e.stopPropagation()
92 | if (!isDragging.current) {
93 | onNodeClick({ config, nodeId: node.id })
94 | }
95 | }
96 | }, [config, node.id])
97 |
98 | const onDoubleClick = React.useCallback((e: React.MouseEvent) => {
99 | if (!config.readonly) {
100 | e.stopPropagation()
101 | if (!isDragging.current) {
102 | onNodeDoubleClick({ config, nodeId: node.id })
103 | }
104 | }
105 | }, [config, node.id])
106 |
107 | const onMouseEnter = React.useCallback(() => {
108 | onNodeMouseEnter({ config, nodeId: node.id })
109 | }, [config, node.id])
110 |
111 | const onMouseLeave = React.useCallback(() => {
112 | onNodeMouseLeave({ config, nodeId: node.id })
113 | }, [config, node.id])
114 |
115 | const compRef = React.useRef(null)
116 |
117 | // TODO: probably should add an observer to track node component size changes
118 | React.useLayoutEffect(() => {
119 | const el = ReactDOM.findDOMNode(compRef.current) as HTMLInputElement
120 | if (el) {
121 | if (
122 | (node.size && node.size.width) !== el.offsetWidth ||
123 | (node.size && node.size.height) !== el.offsetHeight
124 | ) {
125 | const newSize = { width: el.offsetWidth, height: el.offsetHeight }
126 | setSize(newSize)
127 | onNodeSizeChange({ config, nodeId: node.id, size: newSize })
128 | }
129 | }
130 | }, [node, compRef.current, size.width, size.height])
131 |
132 | const children = (
133 | <>
134 | {
136 | const newSize = { width: rect.width, height: rect.height }
137 | setSize(newSize)
138 | }}
139 | />
140 |
141 |
142 | { Object.keys(node.ports).map((portId) => (
143 |
160 | )) }
161 |
162 | >
163 | )
164 |
165 | return (
166 |
176 |
187 |
188 | )
189 | }
190 |
--------------------------------------------------------------------------------
/src/components/Node/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Node.default'
2 | export * from './Node.wrapper'
3 |
--------------------------------------------------------------------------------
/src/components/NodeInner/NodeInner.default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { IConfig, INode } from '../../'
4 |
5 | export interface INodeInnerDefaultProps {
6 | config: IConfig
7 | node: INode
8 | }
9 |
10 | const Outer = styled.div`
11 | padding: 40px 30px;
12 | `
13 |
14 | export const NodeInnerDefault = ({ node }: INodeInnerDefaultProps) => {
15 | return (
16 |
17 | Type: {node.type}
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/NodeInner/index.ts:
--------------------------------------------------------------------------------
1 | export * from './NodeInner.default'
2 |
--------------------------------------------------------------------------------
/src/components/Port/Port.default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { IConfig, IPort } from '../../'
4 |
5 | export interface IPortDefaultProps {
6 | config: IConfig
7 | port: IPort
8 | isSelected: boolean
9 | isHovered: boolean
10 | isLinkSelected: boolean
11 | isLinkHovered: boolean
12 | }
13 |
14 | const PortDefaultOuter = styled.div`
15 | width: 24px;
16 | height: 24px;
17 | border-radius: 50%;
18 | background: white;
19 | cursor: pointer;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | &:hover > div {
24 | background: cornflowerblue;
25 | }
26 | `
27 |
28 | const PortDefaultInner = styled.div<{ active: boolean }>`
29 | width: 12px;
30 | height: 12px;
31 | border-radius: 50%;
32 | background: ${(props) => props.active ? 'cornflowerblue' : 'grey' };
33 | cursor: pointer;
34 | `
35 |
36 | export const PortDefault = ({ isLinkSelected, isLinkHovered, config }: IPortDefaultProps) => (
37 |
38 |
41 |
42 | )
43 |
--------------------------------------------------------------------------------
/src/components/Port/Port.wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { isEqual } from 'lodash'
2 | import * as React from 'react'
3 | import * as ReactDOM from 'react-dom'
4 | import { v4 } from 'uuid'
5 | import { IConfig, ILink, INode, IOnLinkCancel, IOnLinkComplete, IOnLinkMove, IOnLinkStart, IOnPortPositionChange, IPort, IPosition, ISelectedOrHovered } from '../../'
6 | import CanvasContext from '../Canvas/CanvasContext'
7 | import { IPortDefaultProps, PortDefault } from './Port.default'
8 |
9 | /** Construct the composed path by traversing parentElements */
10 | const composedPath = (el: HTMLElement | null) => {
11 | const path: HTMLElement[] = []
12 | while (el) {
13 | path.push(el)
14 | el = el.parentElement
15 | }
16 | return path
17 | }
18 |
19 | export interface IPortWrapperProps {
20 | config: IConfig
21 | style?: object
22 | offset: IPosition
23 | selected: ISelectedOrHovered | undefined
24 | hovered: ISelectedOrHovered | undefined
25 | selectedLink: ILink | undefined
26 | hoveredLink: ILink | undefined
27 | port: IPort
28 | node: INode
29 | onPortPositionChange: IOnPortPositionChange
30 | Component: React.FunctionComponent
31 |
32 | // Link handlers
33 | onLinkStart: IOnLinkStart
34 | onLinkMove: IOnLinkMove
35 | onLinkCancel: IOnLinkCancel
36 | onLinkComplete: IOnLinkComplete
37 | }
38 |
39 | export class PortWrapper extends React.Component {
40 | public static contextType = CanvasContext
41 | public context!: React.ContextType
42 |
43 | private nodeRef = React.createRef()
44 |
45 | public componentDidMount () {
46 | this.updatePortPosition()
47 | }
48 |
49 | public componentDidUpdate (prevProps: IPortWrapperProps) {
50 | // Update port position after a re-render if there are more ports on the same side
51 | // or if node.size has changed
52 | if (this.portsOfType(this.props) !== this.portsOfType(prevProps) || !isEqual(this.props.node.size, prevProps.node.size)) {
53 | this.updatePortPosition()
54 | }
55 | }
56 |
57 | public onMouseDown = (startEvent: React.MouseEvent) => {
58 | const { offset, node, port, onLinkStart, onLinkCancel, onLinkComplete, onLinkMove, config } = this.props
59 | const linkId = v4()
60 | const fromNodeId = node.id
61 | const fromPortId = port.id
62 |
63 | // Create the move handler
64 | // This will update the position as the mouse moves
65 | const mouseMoveHandler = (e: MouseEvent) => {
66 | const { offsetX, offsetY, zoomScale } = this.context
67 |
68 | onLinkMove({
69 | config,
70 | linkId,
71 | startEvent,
72 | fromNodeId,
73 | fromPortId,
74 | toPosition: {
75 | x: (e.clientX - offsetX - offset.x) / zoomScale,
76 | y: (e.clientY - offsetY - offset.y) / zoomScale,
77 | },
78 | })
79 | }
80 |
81 | // Create and bind the mouse up handler
82 | // This is used to check if the link is complete or cancelled
83 | const mouseUpHandler = (e: MouseEvent) => {
84 | // We traverse up the event path until we find an element with 'data-port-id' and data-node-id'
85 | // e.toElement cannot be used because it may be a child element of the port
86 | const path = composedPath(e.target as HTMLElement)
87 | const portEl = path.find((el) => {
88 | const toPortId = el.getAttribute && el.getAttribute('data-port-id')
89 | const toNodeId = el.getAttribute && el.getAttribute('data-node-id')
90 | return !!(toPortId && toNodeId)
91 | })
92 |
93 | // If both node-id and port-id are defined as data attributes, we are mouse-upping
94 | // on another port. Run the success handler
95 | if (portEl) {
96 | const toPortId = portEl.getAttribute('data-port-id') as string
97 | const toNodeId = portEl.getAttribute('data-node-id') as string
98 | onLinkComplete({ config, linkId, startEvent, fromNodeId, fromPortId, toNodeId, toPortId })
99 | } else {
100 | onLinkCancel({ config, linkId, startEvent, fromNodeId, fromPortId })
101 | }
102 |
103 | // Remove the listeners if the link is complete or canceled
104 | window.removeEventListener('mouseup', mouseUpHandler, false)
105 | window.removeEventListener('mousemove', mouseMoveHandler, false)
106 | }
107 |
108 | // Add listeners
109 | window.addEventListener('mouseup', mouseUpHandler, false)
110 | window.addEventListener('mousemove', mouseMoveHandler, false)
111 |
112 | // Notify state of link start
113 | onLinkStart({ config, linkId, startEvent, fromNodeId, fromPortId })
114 |
115 | // Prevent default and stop propagation to prevent text selection
116 | startEvent.preventDefault()
117 | startEvent.stopPropagation()
118 | }
119 | public render () {
120 | const {
121 | selected,
122 | selectedLink,
123 | hovered,
124 | hoveredLink,
125 | style,
126 | port,
127 | node,
128 | Component = PortDefault,
129 | config,
130 | } = this.props
131 |
132 | return (
133 |
140 |
159 |
160 | )
161 | }
162 |
163 | private updatePortPosition () {
164 | const el = ReactDOM.findDOMNode(this.nodeRef.current) as HTMLInputElement
165 | if (el) {
166 | // Ports component should be positions absolute
167 | // Factor this in so we get position relative to the node
168 | const nodesEl = el.parentElement
169 | ? el.parentElement
170 | : { offsetLeft: 0, offsetTop: 0 }
171 | // update port position after node size has been determined
172 | this.props.onPortPositionChange({ config: this.props.config, node: this.props.node, port: this.props.port, el, nodesEl })
173 | }
174 | }
175 |
176 | private portsOfType (props: IPortWrapperProps) {
177 | const { port: { type }, node: { ports } } = props
178 | return Object.values(ports).reduce((count, port) => port.type === type ? count + 1 : count, 0)
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/components/Port/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Port.default'
2 | export * from './Port.wrapper'
3 |
--------------------------------------------------------------------------------
/src/components/Ports/Ports.default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { IConfig, INode, PortsGroupDefault } from '../../'
3 |
4 | export interface IPortsDefaultProps {
5 | config: IConfig
6 | node: INode
7 | children: Array>
8 | }
9 |
10 | export const PortsDefault = ({ children, config }: IPortsDefaultProps) => {
11 | return (
12 |
13 |
14 | {children.filter((child) => ['input', 'top'].includes(child.props.port.type))}
15 |
16 |
17 | {children.filter((child) => ['output', 'bottom'].includes(child.props.port.type))}
18 |
19 |
20 | {children.filter((child) => ['right'].includes(child.props.port.type))}
21 |
22 |
23 | {children.filter((child) => ['left'].includes(child.props.port.type))}
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Ports/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Ports.default'
2 |
--------------------------------------------------------------------------------
/src/components/PortsGroup/PortsGroup.default.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { IConfig } from '../../'
3 |
4 | export interface IPortsGroupDefaultProps {
5 | config: IConfig
6 | side: 'top' | 'bottom' | 'left' | 'right'
7 | }
8 |
9 | export const PortsGroupDefault = styled.div`
10 | position: absolute;
11 | display: flex;
12 | justify-content: center;
13 |
14 | ${(props) => {
15 | if (props.side === 'top') {
16 | return css`
17 | width: 100%;
18 | left: 0;
19 | top: -12px;
20 | flex-direction: row;
21 | > div {
22 | margin: 0 3px;
23 | }
24 | `
25 | } else if (props.side === 'bottom') {
26 | return css`
27 | width: 100%;
28 | left: 0;
29 | bottom: -12px;
30 | flex-direction: row;
31 | > div {
32 | margin: 0 3px;
33 | }
34 | `
35 | } else if (props.side === 'left') {
36 | return css`
37 | height: 100%;
38 | top: 0;
39 | left: -12px;
40 | flex-direction: column;
41 | > div {
42 | margin: 3px 0;
43 | }
44 | `
45 | } else {
46 | return css`
47 | height: 100%;
48 | top: 0;
49 | right: -12px;
50 | flex-direction: column;
51 | > div {
52 | margin: 3px 0;
53 | }
54 | `
55 | }
56 | }}
57 | `
58 |
--------------------------------------------------------------------------------
/src/components/PortsGroup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PortsGroup.default'
2 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Canvas'
2 | export * from './Node'
3 | export * from './NodeInner'
4 | export * from './Port'
5 | export * from './Ports'
6 | export * from './PortsGroup'
7 | export * from './Link'
8 | export * from './FlowChart'
9 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const REACT_FLOW_CHART = 'react-flow-chart'
2 |
--------------------------------------------------------------------------------
/src/container/FlowChartWithState.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FlowChart, IChart, IConfig, IFlowChartComponents } from '../'
3 | import * as actions from './actions'
4 | import mapValues from './utils/mapValues'
5 |
6 | export interface IFlowChartWithStateProps {
7 | initialValue: IChart
8 | Components?: IFlowChartComponents
9 | config?: IConfig
10 | }
11 |
12 | /**
13 | * Flow Chart With State
14 | */
15 | export class FlowChartWithState extends React.Component {
16 | public state: IChart
17 | private stateActions = mapValues(actions, (func: any) =>
18 | (...args: any) => this.setState(func(...args)))
19 |
20 | constructor (props: IFlowChartWithStateProps) {
21 | super(props)
22 | this.state = props.initialValue
23 | }
24 | public render () {
25 | const { Components, config } = this.props
26 |
27 | return (
28 |
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/container/actions.ts:
--------------------------------------------------------------------------------
1 | import { v4 } from 'uuid'
2 | import {
3 | IChart, identity, IOnCanvasClick,
4 | IOnCanvasDrop, IOnDeleteKey, IOnDragCanvas, IOnDragCanvasStop,
5 | IOnDragNode, IOnDragNodeStop, IOnLinkCancel, IOnLinkClick, IOnLinkComplete, IOnLinkMouseEnter, IOnLinkMouseLeave,
6 | IOnLinkMove, IOnLinkStart,IOnNodeClick, IOnNodeDoubleClick, IOnNodeMouseEnter,
7 | IOnNodeMouseLeave, IOnNodeSizeChange, IOnPortPositionChange, IOnZoomCanvas, IStateCallback,
8 | } from '../'
9 | import { rotate } from './utils/rotate'
10 |
11 | function getOffset (config: any, data: any, zoom?: number) {
12 | let offset = { x: data.x, y: data.y }
13 | if (config && config.snapToGrid) {
14 | offset = {
15 | x: Math.round(data.x / 20) * 20,
16 | y: Math.round(data.y / 20) * 20,
17 | }
18 | }
19 | if (zoom) {
20 | offset.x = offset.x / zoom
21 | offset.y = offset.y / zoom
22 | }
23 | return offset
24 | }
25 |
26 | /**
27 | * This file contains actions for updating state after each of the required callbacks
28 | */
29 |
30 | export const onDragNode: IStateCallback = ({ config, event, data, id }) => (chart: IChart) => {
31 | const nodechart = chart.nodes[id]
32 |
33 | if (nodechart) {
34 | const position = getOffset(config, data)
35 | chart.nodes[id] = {
36 | ...nodechart,
37 | position,
38 | }
39 | }
40 |
41 | return chart
42 | }
43 |
44 | export const onDragNodeStop: IStateCallback = () => identity
45 |
46 | export const onDragCanvas: IOnDragCanvas = ({ config, data }) => (chart: IChart): IChart => {
47 | chart.offset = getOffset(config, { x: data.positionX, y: data.positionY })
48 | return chart
49 | }
50 |
51 | export const onDragCanvasStop: IStateCallback = () =>
52 | identity
53 |
54 | export const onLinkStart: IStateCallback = ({ linkId, fromNodeId, fromPortId }) => (chart: IChart): IChart => {
55 | chart.links[linkId] = {
56 | id: linkId,
57 | from: {
58 | nodeId: fromNodeId,
59 | portId: fromPortId,
60 | },
61 | to: {},
62 | }
63 | return chart
64 | }
65 |
66 | export const onLinkMove: IStateCallback = ({ linkId, toPosition }) => (chart: IChart): IChart => {
67 | const link = chart.links[linkId]
68 | link.to.position = toPosition
69 | chart.links[linkId] = { ...link }
70 | return chart
71 | }
72 |
73 | export const onLinkComplete: IStateCallback = (props) => {
74 | const { linkId, fromNodeId, fromPortId, toNodeId, toPortId, config = {} } = props
75 |
76 | return (chart: IChart): IChart => {
77 | if (!config.readonly && (config.validateLink ? config.validateLink({ ...props, chart }) : true) && [fromNodeId, fromPortId].join() !== [toNodeId, toPortId].join()) {
78 | chart.links[linkId].to = {
79 | nodeId: toNodeId,
80 | portId: toPortId,
81 | }
82 | } else {
83 | delete chart.links[linkId]
84 | }
85 | return chart
86 | }
87 | }
88 |
89 | export const onLinkCancel: IStateCallback = ({ linkId }) => (chart: IChart) => {
90 | delete chart.links[linkId]
91 | return chart
92 | }
93 |
94 | export const onLinkMouseEnter: IStateCallback = ({ linkId }) => (chart: IChart) => {
95 | // Set the link to hover
96 | const link = chart.links[linkId]
97 | // Set the connected ports to hover
98 | if (link.to.nodeId && link.to.portId) {
99 | if (chart.hovered.type !== 'link' || chart.hovered.id !== linkId) {
100 | chart.hovered = {
101 | type: 'link',
102 | id: linkId,
103 | }
104 | }
105 | }
106 | return chart
107 | }
108 |
109 | export const onLinkMouseLeave: IStateCallback = ({ linkId }) => (chart: IChart) => {
110 | const link = chart.links[linkId]
111 | // Set the connected ports to hover
112 | if (link.to.nodeId && link.to.portId) {
113 | chart.hovered = {}
114 | }
115 | return chart
116 | }
117 |
118 | export const onLinkClick: IStateCallback = ({ linkId }) => (chart: IChart) => {
119 | if (chart.selected.id !== linkId || chart.selected.type !== 'link') {
120 | chart.selected = {
121 | type: 'link',
122 | id: linkId,
123 | }
124 | }
125 | return chart
126 | }
127 |
128 | export const onCanvasClick: IStateCallback = () => (chart: IChart) => {
129 | if (chart.selected.id) {
130 | chart.selected = {}
131 | }
132 | return chart
133 | }
134 |
135 | export const onNodeMouseEnter: IStateCallback = ({ nodeId }) => (chart: IChart) => {
136 | return {
137 | ...chart,
138 | hovered: {
139 | type: 'node',
140 | id: nodeId,
141 | },
142 | }
143 | }
144 |
145 | export const onNodeMouseLeave: IStateCallback = ({ nodeId }) => (chart: IChart) => {
146 | if (chart.hovered.type === 'node' && chart.hovered.id === nodeId) {
147 | return { ...chart, hovered: {} }
148 | }
149 | return chart
150 | }
151 |
152 | export const onDeleteKey: IStateCallback = () => (chart: IChart) => {
153 | if (chart.selected.type === 'node' && chart.selected.id) {
154 | const node = chart.nodes[chart.selected.id]
155 | // Delete the connected links
156 | Object.keys(chart.links).forEach((linkId) => {
157 | const link = chart.links[linkId]
158 | if (link.from.nodeId === node.id || link.to.nodeId === node.id) {
159 | delete chart.links[link.id]
160 | }
161 | })
162 | // Delete the node
163 | delete chart.nodes[chart.selected.id]
164 | } else if (chart.selected.type === 'link' && chart.selected.id) {
165 | delete chart.links[chart.selected.id]
166 | }
167 | if (chart.selected) {
168 | chart.selected = {}
169 | }
170 | return chart
171 | }
172 |
173 | export const onNodeClick: IStateCallback = ({ nodeId }) => (chart: IChart) => {
174 | if (chart.selected.id !== nodeId || chart.selected.type !== 'node') {
175 | chart.selected = {
176 | type: 'node',
177 | id: nodeId,
178 | }
179 | }
180 | return chart
181 | }
182 |
183 | export const onNodeDoubleClick: IStateCallback = ({ nodeId }) => (chart: IChart) => {
184 | if (chart.selected.id !== nodeId || chart.selected.type !== 'node') {
185 | chart.selected = {
186 | type: 'node',
187 | id: nodeId,
188 | }
189 | }
190 | return chart
191 | }
192 |
193 | export const onNodeSizeChange: IStateCallback = ({ nodeId, size }) => (chart: IChart) => {
194 | chart.nodes[nodeId] = {
195 | ...chart.nodes[nodeId],
196 | size,
197 | }
198 | return chart
199 | }
200 |
201 | export const onPortPositionChange: IStateCallback = ({
202 | node: nodeToUpdate,
203 | port,
204 | el,
205 | nodesEl,
206 | }) => (chart: IChart): IChart => {
207 | if (nodeToUpdate.size) {
208 | // rotate the port's position based on the node's orientation prop (angle)
209 | const center = {
210 | x: nodeToUpdate.size.width / 2,
211 | y: nodeToUpdate.size.height / 2,
212 | }
213 | const current = {
214 | x: el.offsetLeft + nodesEl.offsetLeft + el.offsetWidth / 2,
215 | y: el.offsetTop + nodesEl.offsetTop + el.offsetHeight / 2,
216 | }
217 | const angle = nodeToUpdate.orientation || 0
218 | const position = rotate(center, current, angle)
219 |
220 | const node = chart.nodes[nodeToUpdate.id]
221 | node.ports[port.id].position = {
222 | x: position.x,
223 | y: position.y,
224 | }
225 |
226 | chart.nodes[nodeToUpdate.id] = { ...node }
227 | }
228 |
229 | return chart
230 | }
231 |
232 | export const onCanvasDrop: IStateCallback = ({
233 | config,
234 | data,
235 | position,
236 | }) => (chart: IChart): IChart => {
237 | const id = data.id || v4()
238 | chart.nodes[id] = {
239 | id,
240 | position:
241 | config && config.snapToGrid
242 | ? {
243 | x: Math.round(position.x / 20) * 20,
244 | y: Math.round(position.y / 20) * 20,
245 | }
246 | : { x: position.x, y: position.y },
247 | orientation: data.orientation || 0,
248 | type: data.type,
249 | ports: data.ports,
250 | properties: data.properties,
251 | }
252 | return chart
253 | }
254 |
255 | export const onZoomCanvas: IOnZoomCanvas = ({ config, data }) => (
256 | chart: IChart,
257 | ): IChart => {
258 | chart.offset = getOffset(config, { x: data.positionX, y: data.positionY })
259 | chart.scale = data.scale
260 | return chart
261 | }
262 |
--------------------------------------------------------------------------------
/src/container/index.ts:
--------------------------------------------------------------------------------
1 | import * as _actions from './actions'
2 | export const actions = _actions
3 | export * from './FlowChartWithState'
4 |
--------------------------------------------------------------------------------
/src/container/utils/mapValues.ts:
--------------------------------------------------------------------------------
1 | export default function mapValues<
2 | Obj extends object,
3 | Res extends { [key in keyof Obj]: any }
4 | > (o: Obj, func: (value: Obj[keyof Obj]) => Res[keyof Obj]) {
5 | const res: Res = {} as any
6 | for (const key in o) {
7 | if (o.hasOwnProperty(key)) {
8 | res[key] = func(o[key])
9 | }
10 | }
11 | return res
12 | }
13 |
--------------------------------------------------------------------------------
/src/container/utils/rotate.ts:
--------------------------------------------------------------------------------
1 | import { IPosition } from '../../../'
2 |
3 | // center = rotation center
4 | // current = current position
5 | // x, y = rotated positions
6 | // angle = angle of rotation
7 | export const rotate = (center: IPosition, current: IPosition, angle: number): IPosition => {
8 | const radians = (Math.PI / 180) * angle
9 | const cos = Math.cos(radians)
10 | const sin = Math.sin(radians)
11 | const x = (cos * (current.x - center.x)) + (sin * (current.y - center.y)) + center.x
12 | const y = (cos * (current.y - center.y)) - (sin * (current.x - center.x)) + center.y
13 | return { x, y }
14 | }
15 |
--------------------------------------------------------------------------------
/src/container/utils/validateLink.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sailingdev/React-flow-chart/e33182d3725bab2b5ed25b0e51ea1e1e80498142/src/container/utils/validateLink.ts
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components'
2 | export * from './container'
3 | export * from './types'
4 | export * from './constants'
5 | export * from './utils'
6 |
--------------------------------------------------------------------------------
/src/types/chart.ts:
--------------------------------------------------------------------------------
1 | import { IPosition, ISize } from './generics'
2 |
3 | export interface IChart {
4 | offset: IPosition
5 | nodes: {
6 | [id: string]: INode,
7 | }
8 | links: {
9 | [id: string]: ILink,
10 | }
11 | scale: number
12 | properties?: any
13 |
14 | /** System Temp */
15 | selected: ISelectedOrHovered
16 | hovered: ISelectedOrHovered
17 | }
18 |
19 | export interface ISelectedOrHovered {
20 | type?: 'link' | 'node' | 'port'
21 | id?: string
22 | }
23 |
24 | export interface INode {
25 | id: string
26 | type: string
27 | position: IPosition
28 | orientation?: number
29 | ports: {
30 | [id: string]: IPort,
31 | }
32 | properties?: any
33 | /** System Temp */
34 | size?: ISize
35 | }
36 |
37 | export interface IPort {
38 | id: string
39 | type: string
40 | value?: string
41 | properties?: any
42 | /** System Temp */
43 | position?: IPosition
44 | }
45 |
46 | export interface ILink {
47 | id: string
48 | from: {
49 | nodeId: string
50 | portId: string,
51 | }
52 | to: {
53 | nodeId?: string
54 | portId?: string
55 | /** System Temp */
56 | position?: IPosition,
57 | }
58 | properties?: any
59 | }
60 |
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
1 | import { IChart } from './chart'
2 | import { IOnLinkCompleteInput } from './functions'
3 |
4 | export interface IConfig {
5 | readonly?: boolean
6 | snapToGrid?: boolean
7 | smartRouting?: boolean
8 | gridSize?: number
9 | validateLink?: (props: IOnLinkCompleteInput & { chart: IChart }) => boolean
10 | nodeProps?: any
11 | zoom?: IZoomConfig
12 | [key: string]: any
13 | }
14 |
15 | export interface IZoomConfig {
16 | transformEnabled?: boolean
17 | minScale?: number
18 | maxScale?: number
19 | pan?: {
20 | disabled?: boolean
21 | touchPadEnabled?: boolean,
22 | }
23 | wheel?: {
24 | disabled?: boolean
25 | step?: number
26 | wheelEnabled?: boolean
27 | touchPadEnabled?: boolean,
28 | }
29 | zoomIn?: {
30 | disabled?: boolean
31 | step?: number,
32 | }
33 | zoomOut?: {
34 | disabled?: boolean
35 | step?: number,
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/types/functions.ts:
--------------------------------------------------------------------------------
1 | import { DraggableData, DraggableEvent } from 'react-draggable'
2 | import { IChart, INode, IPort } from './chart'
3 | import { IConfig } from './config'
4 | import { IOffset, IPosition, ISize } from './generics'
5 |
6 | /** Callback functions will be evaluated inside of a setState so they can always manipulate the chart state */
7 | export type IStateCallback any> = (...params: Parameters) => (chart: IChart) => IChart
8 |
9 | export interface IOnDragNodeInput {
10 | config?: IConfig
11 | event: DraggableEvent
12 | data: DraggableData
13 | id: string
14 | }
15 |
16 | export type IOnDragNode = (input: IOnDragNodeInput) => void
17 |
18 | export interface IOnDragCanvasInput {
19 | config?: IConfig
20 | data: any
21 | }
22 |
23 | export type IOnDragCanvas = (input: IOnDragCanvasInput) => void
24 |
25 | export interface IOnDragNodeStopInput {
26 | config?: IConfig
27 | event: MouseEvent
28 | data: DraggableData
29 | id: string
30 | }
31 |
32 | export type IOnDragNodeStop = (input: IOnDragNodeStopInput) => void
33 |
34 | export interface IOnDragCanvasStopInput {
35 | config?: IConfig
36 | data: any
37 | }
38 |
39 | export type IOnDragCanvasStop = (input: IOnDragCanvasStopInput) => void
40 |
41 | export interface IOnPortPositionChangeInput {
42 | config?: IConfig
43 | node: INode
44 | port: IPort
45 | el: HTMLDivElement
46 | nodesEl: HTMLDivElement | IOffset
47 | }
48 |
49 | export type IOnPortPositionChange = (input: IOnPortPositionChangeInput) => void
50 |
51 | export interface ILinkBaseInput {
52 | config?: IConfig
53 | linkId: string
54 | }
55 |
56 | export interface IOnLinkBaseEvent extends ILinkBaseInput {
57 | startEvent: React.MouseEvent
58 | fromNodeId: string
59 | fromPortId: string
60 | }
61 |
62 | export type IOnLinkStart = (input: IOnLinkBaseEvent) => void
63 |
64 | export interface IOnLinkMoveInput extends IOnLinkBaseEvent {
65 | toPosition: {
66 | x: number
67 | y: number,
68 | }
69 | }
70 | export type IOnLinkMove = (input: IOnLinkMoveInput) => void
71 |
72 | export type IOnLinkCancel = (input: IOnLinkBaseEvent) => void
73 |
74 | export interface IOnLinkCompleteInput extends IOnLinkBaseEvent {
75 | toNodeId: string
76 | toPortId: string
77 | }
78 | export type IOnLinkComplete = (input: IOnLinkCompleteInput) => void
79 |
80 | export type IOnLinkMouseEnter = (input: ILinkBaseInput) => void
81 |
82 | export type IOnLinkMouseLeave = (input: ILinkBaseInput) => void
83 |
84 | export type IOnLinkClick = (input: ILinkBaseInput) => void
85 |
86 | export type IOnCanvasClick = (input: { config?: IConfig }) => void
87 |
88 | export type IOnDeleteKey = (input: { config?: IConfig }) => void
89 |
90 | export interface INodeBaseInput {
91 | config?: IConfig
92 | nodeId: string
93 | }
94 |
95 | export type IOnNodeClick = (input: INodeBaseInput) => void
96 |
97 | export type IOnNodeDoubleClick = (input: INodeBaseInput) => void
98 |
99 | export interface IOnNodeSizeChangeInput extends INodeBaseInput {
100 | size: ISize
101 | }
102 |
103 | export type IOnNodeSizeChange = (input: IOnNodeSizeChangeInput) => void
104 |
105 | export type IOnNodeMouseEnter = (input: INodeBaseInput) => void
106 |
107 | export type IOnNodeMouseLeave = (input: INodeBaseInput) => void
108 |
109 | export interface IOnCanvasDropInput {
110 | config?: IConfig
111 | data: any
112 | position: IPosition
113 | }
114 |
115 | export type IOnCanvasDrop = (input: IOnCanvasDropInput) => void
116 |
117 | export type IOnZoomCanvas = (input: { config?: IConfig; data: any }) => void
118 |
--------------------------------------------------------------------------------
/src/types/generics.ts:
--------------------------------------------------------------------------------
1 | export interface IPosition {
2 | x: number
3 | y: number
4 | }
5 |
6 | export interface ISize {
7 | width: number
8 | height: number
9 | }
10 |
11 | export interface IOffset {
12 | offsetLeft: number
13 | offsetTop: number
14 | }
15 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chart'
2 | export * from './config'
3 | export * from './functions'
4 | export * from './generics'
5 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const noop = () => null
2 |
3 | export const identity = (val: T) => val
4 |
--------------------------------------------------------------------------------
/stories/ConfigSnapToGrid.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FlowChartWithState } from '../src'
3 | import { Page } from './components'
4 | import { chartSimple } from './misc/exampleChartState'
5 |
6 | export const ConfigSnapToGridDemo = () => {
7 | return (
8 |
9 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/stories/ConfigValidateLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { FlowChartWithState } from '../src'
4 | import { Page } from './components'
5 | import { chartSimple } from './misc/exampleChartState'
6 |
7 | const Note = styled.div`
8 | position: absolute;
9 | left: 30px;
10 | top: 30px;
11 | padding: 20px;
12 | background: white;
13 | border-radius: 10px;
14 | border: 2px solid red;
15 | `
16 |
17 | export const ConfigValidateLinkDemo = () => {
18 | return (
19 |
20 | {
24 | // no links between same type nodes
25 | if (chart.nodes[fromNodeId].type === chart.nodes[toNodeId].type) return false
26 | return true
27 | },
28 | } }
29 | />
30 | Customise link validation. For example, only allow links between different Node Types
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/stories/CustomCanvasOuter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { FlowChartWithState, ICanvasOuterDefaultProps } from '../src'
4 | import { Page } from './components'
5 | import { chartSimple } from './misc/exampleChartState'
6 |
7 | const CanvasOuterCustom = styled.div`
8 | position: relative;
9 | background-size: 10px 10px;
10 | background-color: #4f6791;
11 | background-image:
12 | linear-gradient(90deg,hsla(0,0%,100%,.1) 1px,transparent 0),
13 | linear-gradient(180deg,hsla(0,0%,100%,.1) 1px,transparent 0);
14 | width: 100%;
15 | height: 100%;
16 | overflow: hidden;
17 | cursor: not-allowed;
18 | ` as any
19 |
20 | export const CustomCanvasOuterDemo = () => {
21 | return (
22 |
23 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/stories/CustomLink.tsx:
--------------------------------------------------------------------------------
1 | import { cloneDeep, mapValues } from 'lodash'
2 | import * as React from 'react'
3 | import styled from 'styled-components'
4 | import { FlowChart, LinkDefault } from '../src'
5 | import * as actions from '../src/container/actions'
6 | import { Page } from './components'
7 | import { chartSimple } from './misc/exampleChartState'
8 |
9 | const Label = styled.div`
10 | position: absolute;
11 | `
12 |
13 | const Button = styled.div`
14 | position: absolute;
15 | top: 0px;
16 | right: 0px;
17 | padding: 5px;
18 | height: 15px;
19 | width: 15px;
20 | transform: translate(50%, -50%);
21 | background: red;
22 | color: white;
23 | border-radius: 50%;
24 | transition: 0.3s ease all;
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | font-size: 10px;
29 | cursor: pointer;
30 | &:hover {
31 | box-shadow: 0 10px 20px rgba(0,0,0,.1);
32 | }
33 | `
34 |
35 | const LabelContent = styled.div`
36 | padding: 5px 10px;
37 | background: cornflowerblue;
38 | color: white;
39 | border-radius: 5px;
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | font-size: 10px;
44 | cursor: pointer;
45 | `
46 |
47 | export class CustomLinkDemo extends React.Component {
48 | public state = cloneDeep(chartSimple)
49 | public render () {
50 | const chart = this.state
51 | const stateActions = mapValues(actions, (func: any) =>
52 | (...args: any) => this.setState(func(...args))) as typeof actions
53 |
54 | return (
55 |
56 | {
61 | const { startPos, endPos, onLinkClick, link } = props
62 | const centerX = startPos.x + (endPos.x - startPos.x) / 2
63 | const centerY = startPos.y + (endPos.y - startPos.y) / 2
64 | return (
65 | <>
66 |
67 |
81 | >
82 | )
83 | },
84 | }}
85 | />
86 |
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/stories/CustomNode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { FlowChartWithState, INodeDefaultProps } from '../src'
4 | import { Page } from './components'
5 | import { chartSimple } from './misc/exampleChartState'
6 |
7 | const DarkBox = styled.div`
8 | position: absolute;
9 | padding: 30px;
10 | background: #3e3e3e;
11 | color: white;
12 | border-radius: 10px;
13 | `
14 |
15 | const Circle = styled.div`
16 | position: absolute;
17 | width: 150px;
18 | height: 150px;
19 | padding: 30px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | background: #d30000;
24 | color: white;
25 | border-radius: 50%;
26 | `
27 |
28 | /**
29 | * Create the custom component,
30 | * Make sure it has the same prop signature
31 | * You'll need to add {...otherProps} so the event listeners are added to your component
32 | */
33 | const NodeCustom = React.forwardRef(({ node, children, ...otherProps }: INodeDefaultProps, ref: React.Ref) => {
34 | if (node.type === 'output-only') {
35 | return (
36 |
37 | {children}
38 |
39 | )
40 | } else {
41 | return (
42 |
43 | {children}
44 |
45 | )
46 | }
47 | })
48 |
49 | export const CustomNodeDemo = () => {
50 | return (
51 |
52 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/stories/CustomNodeInner.tsx:
--------------------------------------------------------------------------------
1 | import { cloneDeep, mapValues } from 'lodash'
2 | import * as React from 'react'
3 | import styled from 'styled-components'
4 | import { FlowChart, INodeInnerDefaultProps } from '../src'
5 | import * as actions from '../src/container/actions'
6 | import { Page } from './components'
7 | import { chartSimple } from './misc/exampleChartState'
8 |
9 | const Outer = styled.div`
10 | padding: 30px;
11 | `
12 |
13 | const Input = styled.input`
14 | padding: 10px;
15 | border: 1px solid cornflowerblue;
16 | width: 100%;
17 | `
18 |
19 | /**
20 | * Create the custom component,
21 | * Make sure it has the same prop signature
22 | */
23 | const NodeInnerCustom = ({ node, config }: INodeInnerDefaultProps) => {
24 | if (node.type === 'output-only') {
25 | return (
26 |
27 | Use Node inner to customise the content of the node
28 |
29 | )
30 | } else {
31 | return (
32 |
33 | Add custom displays for each node.type
34 | You may need to stop event propagation so your forms work.
35 |
36 | console.log(e)}
40 | onClick={(e) => e.stopPropagation()}
41 | onMouseUp={(e) => e.stopPropagation()}
42 | onMouseDown={(e) => e.stopPropagation()}
43 | />
44 |
45 | )
46 | }
47 | }
48 |
49 | export class CustomNodeInnerDemo extends React.Component {
50 | public state = cloneDeep(chartSimple)
51 | public render () {
52 | const chart = this.state
53 | const stateActions = mapValues(actions, (func: any) =>
54 | (...args: any) => this.setState(func(...args))) as typeof actions
55 |
56 | return (
57 |
58 |
65 |
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/stories/CustomPort.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { FlowChartWithState, IPortDefaultProps } from '../src'
4 | import { Page } from './components'
5 | import { chartSimple } from './misc/exampleChartState'
6 |
7 | const PortDefaultOuter = styled.div`
8 | width: 24px;
9 | height: 24px;
10 | background: cornflowerblue;
11 | cursor: pointer;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | `
16 |
17 | const PortCustom = (props: IPortDefaultProps) => (
18 |
19 | { props.port.properties && props.port.properties.value === 'yes' && (
20 |
23 | )}
24 | { props.port.properties && props.port.properties.value === 'no' && (
25 |
28 | )}
29 | { !props.port.properties && (
30 |
33 | )}
34 |
35 | )
36 |
37 | export const CustomPortDemo = () => {
38 | return (
39 |
40 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/stories/DragAndDropSidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { FlowChartWithState } from '../src'
4 | import { Content, Page, Sidebar, SidebarItem } from './components'
5 | import { chartSimple } from './misc/exampleChartState'
6 |
7 | const Message = styled.div`
8 | margin: 10px;
9 | padding: 10px;
10 | background: rgba(0,0,0,0.05);
11 | `
12 |
13 | export const DragAndDropSidebar = () => (
14 |
15 |
16 |
17 |
18 |
19 |
20 | Drag and drop these items onto the canvas.
21 |
22 |
44 |
56 |
75 |
97 |
151 |
152 |
153 | )
154 |
--------------------------------------------------------------------------------
/stories/ExternalReactState.tsx:
--------------------------------------------------------------------------------
1 | import { cloneDeep, mapValues } from 'lodash'
2 | import * as React from 'react'
3 | import { FlowChart } from '../src'
4 | import * as actions from '../src/container/actions'
5 | import { Page } from './components'
6 | import { chartSimple } from './misc/exampleChartState'
7 |
8 | /**
9 | * State is external to the Element
10 | *
11 | * You could easily move this state to Redux or similar by creating your own callback actions.
12 | */
13 | export class ExternalReactState extends React.Component {
14 | public state = cloneDeep(chartSimple)
15 | public render () {
16 | const chart = this.state
17 | const stateActions = mapValues(actions, (func: any) =>
18 | (...args: any) => this.setState(func(...args))) as typeof actions
19 |
20 | return (
21 |
22 |
26 |
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/stories/InternalReactState.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FlowChartWithState } from '../src'
3 | import { Page } from './components'
4 | import { chartSimple } from './misc/exampleChartState'
5 |
6 | export const InternalReactState = () => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/stories/LinkColors.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { FlowChartWithState, IChart } from '../src'
4 | import { Page } from './components'
5 |
6 | const chartSimpleWithLinkColors: IChart = {
7 | offset: {
8 | x: 0,
9 | y: 0,
10 | },
11 | scale: 1,
12 | nodes: {
13 | node1: {
14 | id: 'node1',
15 | type: 'output-only',
16 | position: {
17 | x: 300,
18 | y: 100,
19 | },
20 | ports: {
21 | port1: {
22 | id: 'port1',
23 | type: 'output',
24 | properties: {
25 | value: 'no',
26 | linkColor: '#FFCC00',
27 | },
28 | },
29 | },
30 | },
31 | node2: {
32 | id: 'node2',
33 | type: 'input-output',
34 | position: {
35 | x: 300,
36 | y: 300,
37 | },
38 | ports: {
39 | port1: {
40 | id: 'port1',
41 | type: 'input',
42 | },
43 | port2: {
44 | id: 'port2',
45 | type: 'output',
46 | properties: {
47 | linkColor: '#63D471',
48 | },
49 | },
50 | port3: {
51 | id: 'port3',
52 | type: 'output',
53 | properties: {
54 | linkColor: '#F8333C',
55 | },
56 | },
57 | },
58 | },
59 | node3: {
60 | id: 'node3',
61 | type: 'input-output',
62 | position: {
63 | x: 100,
64 | y: 600,
65 | },
66 | ports: {
67 | port1: {
68 | id: 'port1',
69 | type: 'input',
70 | },
71 | port2: {
72 | id: 'port2',
73 | type: 'output',
74 | },
75 | },
76 | },
77 | node4: {
78 | id: 'node4',
79 | type: 'input-output',
80 | position: {
81 | x: 500,
82 | y: 600,
83 | },
84 | ports: {
85 | port1: {
86 | id: 'port1',
87 | type: 'input',
88 | },
89 | port2: {
90 | id: 'port2',
91 | type: 'output',
92 | },
93 | },
94 | },
95 | },
96 | links: {
97 | link1: {
98 | id: 'link1',
99 | from: {
100 | nodeId: 'node1',
101 | portId: 'port1',
102 | },
103 | to: {
104 | nodeId: 'node2',
105 | portId: 'port1',
106 | },
107 | },
108 | link2: {
109 | id: 'link2',
110 | from: {
111 | nodeId: 'node2',
112 | portId: 'port2',
113 | },
114 | to: {
115 | nodeId: 'node3',
116 | portId: 'port1',
117 | },
118 | },
119 | link3: {
120 | id: 'link3',
121 | from: {
122 | nodeId: 'node2',
123 | portId: 'port3',
124 | },
125 | to: {
126 | nodeId: 'node4',
127 | portId: 'port1',
128 | },
129 | },
130 | },
131 | selected: {},
132 | hovered: {},
133 | }
134 |
135 | export const LinkColors = () => {
136 | return (
137 |
138 |
139 |
140 | )
141 | }
142 |
--------------------------------------------------------------------------------
/stories/ReadonlyMode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FlowChartWithState } from '../src'
3 | import { Page } from './components'
4 | import { chartSimple } from './misc/exampleChartState'
5 |
6 | export const ReadonlyMode = () => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/stories/SelectedSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { cloneDeep, mapValues } from 'lodash'
2 | import * as React from 'react'
3 | import styled from 'styled-components'
4 | import { FlowChart } from '../src'
5 | import * as actions from '../src/container/actions'
6 | import { Content, Page, Sidebar } from './components'
7 | import { chartSimple } from './misc/exampleChartState'
8 |
9 | const Message = styled.div`
10 | margin: 10px;
11 | padding: 10px;
12 | line-height: 1.4em;
13 | `
14 |
15 | const Button = styled.div`
16 | padding: 10px 15px;
17 | background: cornflowerblue;
18 | color: white;
19 | border-radius: 3px;
20 | text-align: center;
21 | transition: 0.3s ease all;
22 | cursor: pointer;
23 | &:hover {
24 | box-shadow: 0 10px 20px rgba(0,0,0,.1);
25 | }
26 | &:active {
27 | background: #5682d2;
28 | }
29 | `
30 |
31 | export class SelectedSidebar extends React.Component {
32 | public state = cloneDeep(chartSimple)
33 | public render () {
34 | const chart = this.state
35 | const stateActions = mapValues(actions, (func: any) =>
36 | (...args: any) => this.setState(func(...args))) as typeof actions
37 |
38 | return (
39 |
40 |
41 |
45 |
46 |
47 | { chart.selected.type
48 | ?
49 | Type: {chart.selected.type}
50 | ID: {chart.selected.id}
51 |
52 | {/*
53 | We can re-use the onDeleteKey action. This will delete whatever is selected.
54 | Otherwise, we have access to the state here so we can do whatever we want.
55 | */}
56 |
57 |
58 | : Click on a Node, Port or Link }
59 |
60 |
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stories/SmartRouting.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { FlowChartWithState } from "../src";
3 | import { Page } from "./components";
4 | import { chartSimple } from "./misc/exampleChartState";
5 |
6 | export const SmartRouting = () => {
7 | return (
8 |
9 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/stories/StressTest.tsx:
--------------------------------------------------------------------------------
1 | import { compact, flatMap, flatten, keyBy, range } from 'lodash'
2 | import * as React from 'react'
3 | import styled from 'styled-components'
4 | import { FlowChartWithState } from '../src'
5 | import { Page } from './components'
6 |
7 | const getChart = (rows: number, cols: number) => {
8 | const xyGrid = flatten(range(0, cols * 300, 300).map((x) => range(0, rows * 150, 150).map((y) => ({ x, y }))))
9 |
10 | return {
11 | offset: {
12 | x: 0,
13 | y: 0,
14 | },
15 | scale: 1,
16 | nodes: keyBy(xyGrid.map(({ x, y }) => ({
17 | id: `node-${x}-${y}`,
18 | type: 'default',
19 | position: { x: x + 100, y: y + 100 },
20 | ports: {
21 | port1: {
22 | id: 'port1',
23 | type: 'top',
24 | },
25 | port2: {
26 | id: 'port2',
27 | type: 'bottom',
28 | },
29 | port3: {
30 | id: 'port3',
31 | type: 'right',
32 | },
33 | port4: {
34 | id: 'port4',
35 | type: 'left',
36 | },
37 | },
38 | })), 'id'),
39 | links: keyBy(compact(flatMap(xyGrid, ({ x, y }, idx) => {
40 | const next = xyGrid[idx + 1]
41 | if (next) {
42 | return [{
43 | id: `link-${x}-${y}-a`,
44 | from: {
45 | nodeId: `node-${x}-${y}`,
46 | portId: 'port2',
47 | },
48 | to: {
49 | nodeId: `node-${next.x}-${next.y}`,
50 | portId: 'port3',
51 | },
52 | }, {
53 | id: `link-${x}-${y}-b`,
54 | from: {
55 | nodeId: `node-${x}-${y}`,
56 | portId: 'port2',
57 | },
58 | to: {
59 | nodeId: `node-${next.x}-${next.y}`,
60 | portId: 'port4',
61 | },
62 | }]
63 | }
64 | return undefined
65 | })), 'id') as any,
66 | selected: {},
67 | hovered: {},
68 | }
69 | }
70 |
71 | const StressTestWithState = () => {
72 | const [rows, setRows] = React.useState(100)
73 | const [cols, setCols] = React.useState(100)
74 |
75 | const chart = React.useMemo(() => getChart(rows, cols), [rows, cols])
76 |
77 | return (
78 | <>
79 |
80 |
81 | setCols(parseInt(e.target.value, 10))} />
82 |
83 | setRows(parseInt(e.target.value, 10))} />
84 |
85 |
86 |
87 |
88 | >
89 | )
90 | }
91 |
92 | export const StressTestDemo = () => {
93 | return
94 | }
95 |
96 | const Input = styled.input`
97 | padding: 5px 5px 5px 10px;
98 | width: 50px;
99 | `
100 |
101 | const Label = styled.label`
102 | padding: 0 10px;
103 | font-size: 14px;
104 | `
105 |
106 | const Controls = styled.div`
107 | padding: 10px;
108 | `
109 |
--------------------------------------------------------------------------------
/stories/Zoom.tsx:
--------------------------------------------------------------------------------
1 | import { cloneDeep, mapValues } from 'lodash'
2 | import * as React from 'react'
3 | import styled from 'styled-components'
4 | import { FlowChart } from '../src'
5 | import * as actions from '../src/container/actions'
6 | import { Content, Page, Sidebar } from './components'
7 | import { chartSimple } from './misc/exampleChartState'
8 |
9 | export const Message = styled.div`
10 | margin: 10px;
11 | padding: 10px;
12 | line-height: 1.4em;
13 | `
14 |
15 | export const Button = styled.div`
16 | padding: 10px;
17 | background: cornflowerblue;
18 | color: white;
19 | border-radius: 3px;
20 | text-align: center;
21 | transition: 0.3s ease all;
22 | margin: 10px;
23 | cursor: pointer;
24 | &:hover {
25 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
26 | }
27 | &:active {
28 | background: #5682d2;
29 | }
30 | `
31 |
32 | export class Zoom extends React.Component {
33 | public state = cloneDeep(chartSimple)
34 | public render () {
35 | const chart = this.state
36 | const stateActions = mapValues(actions, (func: any) => (...args: any) =>
37 | this.setState(func(...args)),
38 | ) as typeof actions
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 | Current zoom:
48 | {chart.scale}
49 |
50 |
51 |
60 |
61 |
70 |
71 |
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/stories/components/Content.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Content = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | flex: 1;
7 | overflow: hidden;
8 | `
9 |
--------------------------------------------------------------------------------
/stories/components/Page.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled, { createGlobalStyle } from 'styled-components'
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | body {
6 | margin: 0px;
7 | max-width: 100vw;
8 | max-height: 100vh;
9 | overflow: hidden;
10 | box-sizing: border-box;
11 | font-family: sans-serif;
12 | }
13 |
14 | *, :after, :before {
15 | box-sizing: inherit;
16 | }
17 | `
18 |
19 | const PageContent = styled.div`
20 | display: flex;
21 | flex-direction: row;
22 | flex: 1;
23 | max-width: 100vw;
24 | max-height: 100vh;
25 | `
26 |
27 | export const Page = ({ children }: { children: any}) => (
28 |
29 | {children}
30 |
31 |
32 | )
33 |
--------------------------------------------------------------------------------
/stories/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Sidebar = styled.div`
4 | width: 300px;
5 | background: white;
6 | display: flex;
7 | flex-direction: column;
8 | flex-shrink: 0;
9 | `
10 |
--------------------------------------------------------------------------------
/stories/components/SidebarItem.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { INode, REACT_FLOW_CHART } from '../../src'
4 |
5 | const Outer = styled.div`
6 | padding: 20px 30px;
7 | font-size: 14px;
8 | background: white;
9 | cursor: move;
10 | `
11 |
12 | export interface ISidebarItemProps {
13 | type: string,
14 | ports: INode['ports'],
15 | properties?: any,
16 | }
17 |
18 | export const SidebarItem = ({ type, ports, properties }: ISidebarItemProps) => {
19 | return (
20 | {
23 | event.dataTransfer.setData(REACT_FLOW_CHART, JSON.stringify({ type, ports, properties }))
24 | } }
25 | >
26 | {type}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/stories/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Content'
2 | export * from './Page'
3 | export * from './Sidebar'
4 | export * from './SidebarItem'
5 |
--------------------------------------------------------------------------------
/stories/index.tsx:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/react'
2 | import * as React from 'react'
3 |
4 | import { ConfigSnapToGridDemo } from './ConfigSnapToGrid'
5 | import { ConfigValidateLinkDemo } from './ConfigValidateLink'
6 | import { CustomCanvasOuterDemo } from './CustomCanvasOuter'
7 | import { CustomLinkDemo } from './CustomLink'
8 | import { CustomNodeDemo } from './CustomNode'
9 | import { CustomNodeInnerDemo } from './CustomNodeInner'
10 | import { CustomPortDemo } from './CustomPort'
11 | import { DragAndDropSidebar } from './DragAndDropSidebar'
12 | import { ExternalReactState } from './ExternalReactState'
13 | import { InternalReactState } from './InternalReactState'
14 | import { LinkColors } from './LinkColors'
15 | import { ReadonlyMode } from './ReadonlyMode'
16 | import { SelectedSidebar } from './SelectedSidebar'
17 | import { SmartRouting } from './SmartRouting'
18 | import { StressTestDemo } from './StressTest'
19 | import { Zoom } from './Zoom'
20 |
21 | storiesOf('State', module)
22 | .add('Internal React State', InternalReactState)
23 | .add('External React State', () => )
24 |
25 | storiesOf('Custom Components', module)
26 | .add('Node Inner', () => )
27 | .add('Node', CustomNodeDemo)
28 | .add('Port', CustomPortDemo)
29 | .add('Canvas Outer', CustomCanvasOuterDemo)
30 | .add('Canvas Link', () => )
31 | .add('Link Colors', () => )
32 |
33 | storiesOf('Stress Testing', module).add('default', StressTestDemo)
34 |
35 | storiesOf('Sidebar', module)
36 | .add('Drag and Drop', DragAndDropSidebar)
37 | .add('Selected Sidebar', () => )
38 |
39 | storiesOf('Other Config', module)
40 | .add('Snap To Grid', ConfigSnapToGridDemo)
41 | .add('Link validation function', ConfigValidateLinkDemo)
42 | .add('Read only mode', ReadonlyMode)
43 | .add('Smart link routing', SmartRouting)
44 | .add('Zoom', () => )
45 |
--------------------------------------------------------------------------------
/stories/misc/exampleChartState.ts:
--------------------------------------------------------------------------------
1 | import { IChart } from '../../src'
2 |
3 | export const chartSimple: IChart = {
4 | offset: {
5 | x: 0,
6 | y: 0,
7 | },
8 | scale: 1,
9 | nodes: {
10 | node1: {
11 | id: 'node1',
12 | type: 'output-only',
13 | position: {
14 | x: 300,
15 | y: 100,
16 | },
17 | ports: {
18 | port1: {
19 | id: 'port1',
20 | type: 'output',
21 | properties: {
22 | value: 'yes',
23 | },
24 | },
25 | port2: {
26 | id: 'port2',
27 | type: 'output',
28 | properties: {
29 | value: 'no',
30 | },
31 | },
32 | },
33 | },
34 | node2: {
35 | id: 'node2',
36 | type: 'input-output',
37 | position: {
38 | x: 300,
39 | y: 300,
40 | },
41 | ports: {
42 | port1: {
43 | id: 'port1',
44 | type: 'input',
45 | },
46 | port2: {
47 | id: 'port2',
48 | type: 'output',
49 | },
50 | },
51 | },
52 | node3: {
53 | id: 'node3',
54 | type: 'input-output',
55 | position: {
56 | x: 100,
57 | y: 600,
58 | },
59 | ports: {
60 | port1: {
61 | id: 'port1',
62 | type: 'input',
63 | },
64 | port2: {
65 | id: 'port2',
66 | type: 'output',
67 | },
68 | },
69 | },
70 | node4: {
71 | id: 'node4',
72 | type: 'input-output',
73 | position: {
74 | x: 500,
75 | y: 600,
76 | },
77 | ports: {
78 | port1: {
79 | id: 'port1',
80 | type: 'input',
81 | },
82 | port2: {
83 | id: 'port2',
84 | type: 'output',
85 | },
86 | },
87 | },
88 | },
89 | links: {
90 | link1: {
91 | id: 'link1',
92 | from: {
93 | nodeId: 'node1',
94 | portId: 'port2',
95 | },
96 | to: {
97 | nodeId: 'node2',
98 | portId: 'port1',
99 | },
100 | properties: {
101 | label: 'example link label',
102 | },
103 | },
104 | link2: {
105 | id: 'link2',
106 | from: {
107 | nodeId: 'node2',
108 | portId: 'port2',
109 | },
110 | to: {
111 | nodeId: 'node3',
112 | portId: 'port1',
113 | },
114 | properties: {
115 | label: 'another example link label',
116 | },
117 | },
118 | link3: {
119 | id: 'link3',
120 | from: {
121 | nodeId: 'node2',
122 | portId: 'port2',
123 | },
124 | to: {
125 | nodeId: 'node4',
126 | portId: 'port1',
127 | },
128 | },
129 | },
130 | selected: {},
131 | hovered: {},
132 | }
133 |
--------------------------------------------------------------------------------
/stories/utils/throttleRender.tsx:
--------------------------------------------------------------------------------
1 | import { throttle } from 'lodash'
2 | import * as React from 'react'
3 |
4 | /** A little HOC to throttle component renders */
5 | export const throttleRender = (wait: number, options?: any) => {
6 | return (ComponentToThrottle: any) => {
7 | return class Throttle extends React.Component {
8 | public throttledSetState: any
9 | constructor (props: any, context: any) {
10 | super(props, context)
11 | this.state = {}
12 | this.throttledSetState = throttle((nextState: any) => this.setState(nextState), wait, options)
13 | }
14 | public shouldComponentUpdate (nextProps: any, nextState: any) {
15 | return this.state !== nextState
16 | }
17 | public componentWillMount () {
18 | this.throttledSetState({ props: this.props })
19 | }
20 | public componentWillReceiveProps (nextProps: any) {
21 | this.throttledSetState({ props: nextProps })
22 | }
23 | public componentWillUnmount () {
24 | this.throttledSetState.cancel()
25 | }
26 | public render () {
27 | return
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist/src",
4 | "module": "commonjs",
5 | "target": "es5",
6 | "lib": ["es5", "es6", "es7", "es2017", "dom"],
7 | "sourceMap": true,
8 | "jsx": "react",
9 | "moduleResolution": "node",
10 | "rootDirs": ["src", "stories"],
11 | "baseUrl": "src",
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitReturns": true,
14 | "noImplicitThis": true,
15 | "noImplicitAny": true,
16 | "strictNullChecks": true,
17 | "suppressImplicitAnyIndexErrors": true,
18 | "noUnusedLocals": true,
19 | "allowSyntheticDefaultImports": true,
20 | "experimentalDecorators": true,
21 | "allowJs": false,
22 | "declaration": true,
23 | "types": ["react"]
24 | },
25 | "include": ["src/index.ts"],
26 | "exclude": ["node_modules", "build", "scripts", "config"]
27 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:latest",
4 | "tslint-config-standard",
5 | "tslint-react"
6 | ],
7 | "rules": {
8 | "semicolon": [true, "never"],
9 | "object-literal-sort-keys": false,
10 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}],
11 | "jsx-no-lambda": false,
12 | "jsx-no-multiline-js": false,
13 | "quotemark": [true, "single", "jsx-double"],
14 | "no-implicit-dependencies": [true, "dev"],
15 | "no-console": [false],
16 | "max-line-length": [true, 200]
17 | }
18 | }
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*';
2 |
3 | declare module 'react-json-view' {
4 | const value: any;
5 | export = value;
6 | }
7 |
--------------------------------------------------------------------------------