├── .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 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 1600962472997 51 | 56 | 57 | 1600962616778 58 | 63 | 64 | 1600962779247 65 | 70 | 73 | 74 | 76 | 77 | 98 | 99 | 100 | 101 | 102 | 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 | ![demo](./images/demo.gif) 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 | ![Demo](./images/demo.png) 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 | Storybook

No 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 | 42 | 48 | {/* Main line */} 49 | 55 | {/* Thick line to make selection easier */} 56 | onLinkMouseEnter({ config, linkId: link.id })} 64 | onMouseLeave={() => onLinkMouseLeave({ config, linkId: link.id })} 65 | onClick={(e) => { 66 | onLinkClick({ config, linkId: link.id }) 67 | e.stopPropagation() 68 | } } 69 | /> 70 | 76 | 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 | 21 | 22 | 23 | )} 24 | { props.port.properties && props.port.properties.value === 'no' && ( 25 | 26 | 27 | 28 | )} 29 | { !props.port.properties && ( 30 | 31 | 32 | 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 | --------------------------------------------------------------------------------