Repository: projectstorm/react-diagrams Branch: master Commit: cd3bdd119376 Files: 229 Total size: 342.7 KB Directory structure: gitextract_auqij3jd/ ├── .changeset/ │ ├── README.md │ ├── cold-drinks-unite.md │ └── config.json ├── .editorconfig ├── .envrc ├── .gitbook.yaml ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── prettier.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── diagrams-demo-gallery/ │ ├── .babelrc.json │ ├── .storybook/ │ │ ├── main.js │ │ ├── manager.js │ │ ├── preview-head.html │ │ ├── preview.js │ │ └── theme.js │ ├── CHANGELOG.md │ ├── demos/ │ │ ├── 1_SimpleUsage.stories.tsx │ │ ├── 2_AdvancedUsage.stories.tsx │ │ ├── 3_Customization.stories.tsx │ │ ├── 4_Libraries.stories.tsx │ │ ├── demo-alternative-linking/ │ │ │ ├── CreateLinkState.ts │ │ │ ├── DefaultState.ts │ │ │ └── index.tsx │ │ ├── demo-animation/ │ │ │ └── index.tsx │ │ ├── demo-canvas-drag/ │ │ │ └── index.tsx │ │ ├── demo-cloning/ │ │ │ └── index.tsx │ │ ├── demo-custom-action/ │ │ │ └── index.tsx │ │ ├── demo-custom-link-label/ │ │ │ ├── EditableLabelFactory.tsx │ │ │ ├── EditableLabelModel.ts │ │ │ ├── EditableLabelWidget.tsx │ │ │ └── index.tsx │ │ ├── demo-custom-link1/ │ │ │ └── index.tsx │ │ ├── demo-custom-link2/ │ │ │ └── index.tsx │ │ ├── demo-custom-node1/ │ │ │ ├── DiamondNodeFactory.tsx │ │ │ ├── DiamondNodeModel.ts │ │ │ ├── DiamondNodeWidget.tsx │ │ │ ├── DiamondPortModel.ts │ │ │ ├── SimplePortFactory.ts │ │ │ └── index.tsx │ │ ├── demo-custom_delete_keys/ │ │ │ └── index.tsx │ │ ├── demo-dagre/ │ │ │ └── index.tsx │ │ ├── demo-drag-and-drop/ │ │ │ ├── Application.ts │ │ │ ├── components/ │ │ │ │ ├── BodyWidget.tsx │ │ │ │ ├── TrayItemWidget.tsx │ │ │ │ └── TrayWidget.tsx │ │ │ └── index.tsx │ │ ├── demo-dynamic-ports/ │ │ │ └── index.tsx │ │ ├── demo-grid/ │ │ │ └── index.tsx │ │ ├── demo-labelled-links/ │ │ │ └── index.tsx │ │ ├── demo-listeners/ │ │ │ └── index.tsx │ │ ├── demo-locks/ │ │ │ └── index.tsx │ │ ├── demo-mutate-graph/ │ │ │ └── index.tsx │ │ ├── demo-pan-and-zoom/ │ │ │ └── index.tsx │ │ ├── demo-performance/ │ │ │ └── index.tsx │ │ ├── demo-right-angles-routing/ │ │ │ └── index.tsx │ │ ├── demo-serializing/ │ │ │ └── index.tsx │ │ ├── demo-simple/ │ │ │ └── index.tsx │ │ ├── demo-simple-flow/ │ │ │ └── index.tsx │ │ ├── demo-smart-routing/ │ │ │ └── index.tsx │ │ ├── demo-zoom-to-fit/ │ │ │ └── index.tsx │ │ ├── demo-zoom-to-fit-nodes/ │ │ │ └── index.tsx │ │ └── helpers/ │ │ ├── DemoCanvasWidget.tsx │ │ ├── DemoWorkspaceWidget.tsx │ │ ├── Helper.tsx │ │ └── index.css │ ├── package.json │ └── tsconfig.json ├── diagrams-demo-project/ │ ├── .babelrc │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── BodyWidget.tsx │ │ ├── custom-node-js/ │ │ │ ├── JSCustomNodeFactory.jsx │ │ │ ├── JSCustomNodeModel.js │ │ │ └── JSCustomNodeWidget.jsx │ │ ├── custom-node-ts/ │ │ │ ├── TSCustomNodeFactory.tsx │ │ │ ├── TSCustomNodeModel.ts │ │ │ └── TSCustomNodeWidget.tsx │ │ ├── main.css │ │ └── main.tsx │ ├── tsconfig.json │ └── webpack.config.js ├── docs/ │ ├── README.md │ ├── about-the-project/ │ │ ├── architecture-questions.md │ │ └── testing.md │ ├── customizing/ │ │ ├── README.md │ │ ├── extending-default-links.md │ │ ├── nodes.md │ │ └── ports.md │ └── getting-started/ │ ├── README.md │ └── using-the-library.md ├── package.json ├── packages/ │ ├── geometry/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── BezierCurve.ts │ │ │ ├── Bounds.ts │ │ │ ├── Matrix.ts │ │ │ ├── Point.ts │ │ │ ├── Polygon.ts │ │ │ ├── Rectangle.ts │ │ │ ├── index.ts │ │ │ └── toolkit.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── react-canvas-core/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── CanvasEngine.ts │ │ │ ├── Toolkit.ts │ │ │ ├── actions/ │ │ │ │ ├── DeleteItemsAction.ts │ │ │ │ ├── PanAndZoomCanvasAction.ts │ │ │ │ └── ZoomCanvasAction.ts │ │ │ ├── core/ │ │ │ │ ├── AbstractFactory.ts │ │ │ │ ├── AbstractModelFactory.ts │ │ │ │ ├── AbstractReactFactory.tsx │ │ │ │ ├── BaseObserver.ts │ │ │ │ ├── FactoryBank.ts │ │ │ │ └── ModelGeometryInterface.ts │ │ │ ├── core-actions/ │ │ │ │ ├── Action.ts │ │ │ │ └── ActionEventBus.ts │ │ │ ├── core-models/ │ │ │ │ ├── BaseEntity.ts │ │ │ │ ├── BaseModel.ts │ │ │ │ └── BasePositionModel.ts │ │ │ ├── core-state/ │ │ │ │ ├── AbstractDisplacementState.ts │ │ │ │ ├── State.ts │ │ │ │ └── StateMachine.ts │ │ │ ├── entities/ │ │ │ │ ├── canvas/ │ │ │ │ │ ├── CanvasModel.ts │ │ │ │ │ └── CanvasWidget.tsx │ │ │ │ ├── layer/ │ │ │ │ │ ├── LayerModel.ts │ │ │ │ │ ├── SmartLayerWidget.tsx │ │ │ │ │ └── TransformLayerWidget.tsx │ │ │ │ └── selection/ │ │ │ │ ├── SelectionBoxLayerFactory.tsx │ │ │ │ ├── SelectionBoxWidget.tsx │ │ │ │ └── SelectionLayerModel.ts │ │ │ ├── index.ts │ │ │ ├── states/ │ │ │ │ ├── DefaultState.ts │ │ │ │ ├── DragCanvasState.ts │ │ │ │ ├── MoveItemsState.ts │ │ │ │ ├── SelectingState.ts │ │ │ │ └── SelectionBoxState.ts │ │ │ └── widgets/ │ │ │ └── PeformanceWidget.tsx │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── react-diagrams/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── react-diagrams-core/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── DiagramEngine.ts │ │ │ ├── entities/ │ │ │ │ ├── label/ │ │ │ │ │ ├── LabelModel.ts │ │ │ │ │ └── LabelWidget.tsx │ │ │ │ ├── link/ │ │ │ │ │ ├── LinkModel.ts │ │ │ │ │ ├── LinkWidget.tsx │ │ │ │ │ └── PointModel.ts │ │ │ │ ├── link-layer/ │ │ │ │ │ ├── LinkLayerFactory.tsx │ │ │ │ │ ├── LinkLayerModel.ts │ │ │ │ │ └── LinkLayerWidget.tsx │ │ │ │ ├── node/ │ │ │ │ │ ├── NodeModel.ts │ │ │ │ │ └── NodeWidget.tsx │ │ │ │ ├── node-layer/ │ │ │ │ │ ├── NodeLayerFactory.tsx │ │ │ │ │ ├── NodeLayerModel.ts │ │ │ │ │ └── NodeLayerWidget.tsx │ │ │ │ └── port/ │ │ │ │ ├── PortModel.ts │ │ │ │ └── PortWidget.tsx │ │ │ ├── index.ts │ │ │ ├── models/ │ │ │ │ └── DiagramModel.ts │ │ │ └── states/ │ │ │ ├── DefaultDiagramState.ts │ │ │ ├── DragDiagramItemsState.ts │ │ │ └── DragNewLinkState.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── react-diagrams-defaults/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── label/ │ │ │ │ ├── DefaultLabelFactory.tsx │ │ │ │ ├── DefaultLabelModel.tsx │ │ │ │ └── DefaultLabelWidget.tsx │ │ │ ├── link/ │ │ │ │ ├── DefaultLinkFactory.tsx │ │ │ │ ├── DefaultLinkModel.ts │ │ │ │ ├── DefaultLinkPointWidget.tsx │ │ │ │ ├── DefaultLinkSegmentWidget.tsx │ │ │ │ └── DefaultLinkWidget.tsx │ │ │ ├── node/ │ │ │ │ ├── DefaultNodeFactory.tsx │ │ │ │ ├── DefaultNodeModel.ts │ │ │ │ └── DefaultNodeWidget.tsx │ │ │ └── port/ │ │ │ ├── DefaultPortFactory.tsx │ │ │ ├── DefaultPortLabelWidget.tsx │ │ │ └── DefaultPortModel.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ └── react-diagrams-routing/ │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src/ │ │ ├── dagre/ │ │ │ └── DagreEngine.ts │ │ ├── engine/ │ │ │ └── PathFinding.ts │ │ ├── index.ts │ │ └── link/ │ │ ├── PathFindingLinkFactory.tsx │ │ ├── PathFindingLinkModel.ts │ │ ├── PathFindingLinkWidget.tsx │ │ ├── RightAngleLinkFactory.tsx │ │ ├── RightAngleLinkModel.ts │ │ └── RightAngleLinkWidget.tsx │ ├── tests/ │ │ └── PathFinding.test.tsx │ ├── tsconfig.json │ └── webpack.config.js ├── pnpm-workspace.yaml ├── tsconfig.base.json ├── tsconfig.json └── webpack.shared.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/cold-drinks-unite.md ================================================ --- '@projectstorm/react-diagrams-defaults': patch '@projectstorm/react-diagrams-routing': patch '@projectstorm/react-diagrams-core': patch '@projectstorm/react-canvas-core': patch '@projectstorm/react-diagrams-gallery': patch '@projectstorm/react-diagrams-demo': patch '@projectstorm/geometry': patch --- Updated packages to support React v19 ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [], "access": "restricted", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .editorconfig ================================================ [*] indent_style = tab indent_size = 2 trim_trailing_whitespace = true # Some exceptions [{package.json,*.yml}] indent_style = space indent_size = 2 ================================================ FILE: .envrc ================================================ PATH_add ./node_modules/.bin ================================================ FILE: .gitbook.yaml ================================================ root: ./docs/ structure: summary: README.md ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Checklist - [ ] The tests pass - [ ] I have referenced the issue(s) or other PR(s) this fixes/relates-to - [ ] I have run ```pnpm changeset``` and followed the instructions - [ ] I have explained in this PR, what I did and why - [ ] I replaced the image below - [ ] Had a beer/coffee/tea because I did something cool today ## What, why and how? ## Feel good image: ![LOL](https://i.pinimg.com/originals/7f/1b/c3/7f1bc3fb2e123dd3255a85c04db22f19.jpg) ================================================ FILE: .github/workflows/prettier.yml ================================================ name: Prettier check # This action works with pull requests and pushes on: pull_request: jobs: prettier: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: # Make sure the actual branch is checked out when running on pull requests ref: ${{ github.head_ref }} - uses: actions/checkout@v2 # Check out the repository first. - uses: actionsx/prettier@v2 with: # prettier CLI arguments. args: --check --ignore-path .prettierignore --config .prettierrc '**/*.{ts,tsx,js,jsx}' ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - master concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v2 - name: Read .nvmrc run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" id: nvm - name: Use Node.js (.nvmrc) uses: actions/setup-node@v2 with: node-version: "${{ steps.nvm.outputs.NVMRC }}" - name: Install PNPM uses: pnpm/action-setup@v2 with: version: latest - name: Install Dependencies run: pnpm install - name: Create Release Pull Request uses: changesets/action@v1 id: changesets with: # This expects you to have a script called release which does a build for your packages and calls changeset publish publish: pnpm release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: publish storybook if: steps.changesets.outputs.published == 'true' run: pnpm release:storybook ================================================ FILE: .github/workflows/test.yml ================================================ name: Build and Test on: pull_request: jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v2 - name: Read .nvmrc run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" id: nvm - name: Use Node.js (.nvmrc) uses: actions/setup-node@v2 with: node-version: "${{ steps.nvm.outputs.NVMRC }}" - name: Install PNPM uses: pnpm/action-setup@v2 with: version: latest - name: Install Dependencies run: pnpm install - name: Build run: pnpm build ================================================ FILE: .gitignore ================================================ dist .DS_Store .idea .out *.zip .env node_modules tsconfig.tsbuildinfo .vscode ================================================ FILE: .prettierignore ================================================ node_modules dist .out ================================================ FILE: .prettierrc ================================================ { "semi": true, "singleQuote": true, "useTabs": true, "printWidth": 120, "trailingComma": "none" } ================================================ FILE: CHANGELOG.md ================================================ __V7!__ we are now using changesets! you can see the changes for individual packages in their corresponding folders. Here is the main changeset for the core package which depends on everything: [Changelog for @projectstorm/react-diagrams](./packages/react-diagrams/CHANGELOG.md) --- __6.7.4__ .0 -> .4 because I messed up the version / publishing * (upgrade all dependencies, including a move to React 18) * https://github.com/projectstorm/react-diagrams/pull/947 __6.7.0__ bug fixes: * https://github.com/projectstorm/react-diagrams/pull/882 * https://github.com/projectstorm/react-diagrams/pull/914 * https://github.com/projectstorm/react-diagrams/pull/875 types * https://github.com/projectstorm/react-diagrams/pull/906 features: * https://github.com/projectstorm/react-diagrams/pull/915 * https://github.com/projectstorm/react-diagrams/pull/877 __6.6.1__ bug fixes: * https://github.com/projectstorm/react-diagrams/pull/861 * https://github.com/projectstorm/react-diagrams/pull/871 * https://github.com/projectstorm/react-diagrams/pull/870 Some maintenance: * https://github.com/projectstorm/react-diagrams/pull/861 __6.6.0__ * (docs-broken) https://github.com/projectstorm/react-diagrams/pull/834 * (bug) https://github.com/projectstorm/react-diagrams/pull/838 * (docs-broken) https://github.com/projectstorm/react-diagrams/pull/847 * (bug) https://github.com/projectstorm/react-diagrams/pull/852 * (docs-broken) https://github.com/projectstorm/react-diagrams/pull/856 * (improvement) https://github.com/projectstorm/react-diagrams/pull/857 * (bug) https://github.com/projectstorm/react-diagrams/pull/860 Also includes a bump on all packages using `ncu` recursively. __6.5.2__ https://github.com/projectstorm/react-diagrams/pull/830 * (fix) issue with zoom to fit selected * (improvement) properly export PathFinding * (maintenance) bump all dependencies __6.5.1__ https://github.com/projectstorm/react-diagrams/pull/829 * (improved) zoom to fit now centers correctly * (fix) remove wrong peer dependency (@emotion/core) __6.5.0__ https://github.com/projectstorm/react-diagrams/pull/814 * Some rendering fixes * small api change around `zoomToFit` * more api options with the `DefaultLink` __6.4.0__ https://github.com/projectstorm/react-diagrams/pull/813 * Bump all packages and move to Emotion 11 and React 17 * Move to the latest Storybook __6.2.0__ * (improvement) Move away fromn math-js (https://github.com/projectstorm/react-diagrams/pull/651) * (fix) https://github.com/projectstorm/react-diagrams/pull/639 * (fix) Fixing link spawning at (0,0) when clicking port once (inspired by https://github.com/projectstorm/react-diagrams/pull/637) __6.1.1__ * (feature) https://github.com/projectstorm/react-diagrams/pull/576 [Add zoom to fit nodes feature, fixes #568] * (improvement) https://github.com/projectstorm/react-diagrams/pull/621 [Support deriving from DefaultLabelModel] * (fix) https://github.com/projectstorm/react-diagrams/pull/603 [Fixes selectionChanged listener not being deregistered on NodeWidget, Fixes unchecked access to this.props.link.getSourcePort() on LinkWidget] * (maintenance) bump everything * fix serialize/deserialize issue with example project raw JS node __6.0.0__ Note: This is a complete rewrite of the library, a good place to start to see how the new system works is with the new demo project which illustrates the new capability. I would also recommend taking a look at the new updated DiamondPort widget which shows more capability. * Break up library into monorepo * Introduce react-canvas-core as a new framework * Use geometry classes instead of raw X and Y primitives so we can do matrix stuff in the future * move testing framework to a name based system instead of ID's * Introduce multiple layers (can now have multiple node and link layers) * Rewrote the deserialization system to be promise based * Completely overhauled the observer framework on the models * Moved all the logic in the DiagramWidget into a a new hierarchical state machine * Introduces new states for editing * Introduced faster layout rendering when transforming the canvas directly * Moved all canvas smart routing into its own link-type under routing package * Broke up link rendering into a much more modular system that is much easier to extend * Introduced port alignment allowing the developer to specify how enter it * Improved generics throughout the entire model system with Mapped Types * Rewrote all the styles using emotion instead of sass * Fixed up all the demos to use the new API * Introduced a demo project that illustrates how to use the library with ES6 as well as with Typescript * Improved the grid rendering system to allow graphical elements to specify how they get transformed * Introduced a performance widget for improving performance in a more deterministic way by comparing the serialization of the model (with a way of opting out) * Renamed a bunch of methods to be more consistent and more understandable * Completely removed the double render state system that required nodes to render before links, this is done when ports report their new positions * Ports can now dynamically be added and removed without having to tell the system it happeend * Port widgets are now containers dumb containers for you own ports * Port widgets report new sizing information to their target links when they change position, you no longer need to invalidate them __5.3.2__ * (maintenance) Upgrade :allthethings: (all the build tooling was upgrade) * (api) move to ES6 (JS now contains native classes) * (api) changed package name to @projectstorm/react-diagrams * (bug) (PR259)(https://github.com/projectstorm/react-diagrams/pull/259) Fixes #258 * (refactor) (PR 306)(https://github.com/projectstorm/react-diagrams/pull/306) `:any` fix * (feature) (PR 178)(https://github.com/projectstorm/react-diagrams/pull/178) Trigger a positionChanged event when moving a Node that has the listener assigned. * (fix) (PR 356)(https://github.com/projectstorm/react-diagrams/pull/356) Fixed Type issue with 'PointModel()' * (demo) dark mode and upgrade storybook __5.2.1__ * (fix) Always remove link from old source/target port on port change * (maintenance) upgrade node modules * (refactor) https://github.com/projectstorm/react-diagrams/commit/55f62587bd3b12513c7d37eff59edfc8bdb8d6c9 * (bug) https://github.com/projectstorm/react-diagrams/commit/75ef02dd4d131a0e7c08b2680c69efc390e50b84 -> and other improvements, also checkout the foundation work happening over at https://github.com/projectstorm/react-canvas __5.1.0__ * (api) Rename XXXFactory into AbstractXXXFactory * (refactor) tslint and prettier are now the same * (refactor) Each class now explicitely has its own class file (consistency) * (feature) Smooth vertical links (no longer limited to horizontal) * (feature) Dedicated documentation via gitbook * (bug) forgot to export some * (refactor) consistently use lodash where possible * (maintenance) upgrade node modules __5.0.0__ [http://dylanv.blog/2018/03/03/storm-react-diagrams-5-0-0/](https://dylanvorster.com/storm-react-diagrams-v5-0/) PR: https://github.com/projectstorm/react-diagrams/pull/145 * (refactor) Links completely overhauled * (feature) Smart Routing * (feature) Flow support * (demo) Smart Routing * (demo) Animated links * (api) Bootstrapping Improvements * (feature) add custom properties to all widgets * (refactor) use BEM for all css * (feature) Default Link factory hooks * (tests) e2e tests + helper framework * (tests) automatically load JEST Snapshots * (feature) Link labels! __4.0.0__ [http://dylanv.blog/2018/01/18/storm-react-diagrams-v4-0-0/](https://dylanvorster.com/storm-react-diagrams-v4-0/) * (refactor) Events system was completely overhauled * (demo) Custom Link Sizes * (refactor) Demos are now much more verbose and better managed * (update) node packages * (bug) Fix #129 * (feature) Control link creation through ports * (refactor) Models are now in seperate files * (refactor) Merged the concept of instance factories and widget factories into one * (feature) Models can now be cloned at various parts of the model graph * (demo) Cloning * (feature) models control isLocked __3.2.0__ [http://dylanv.blog/2017/11/22/storm-react-diagrams-3-2-0/](https://dylanvorster.com/storm-react-diagrams-3-2-0/) * (feature) zoom to fit * added Circle CI tests * (demo) dagre automatic layouts * (demo) zoom to fit * (demo) selection events * (demo) limit number of points * (demo) programmatic node updating * updated dependencies * (bugs) swapping diagram models in engines * (bugs) issues with the rendering pipeline #107 * added ci badge to Readme __3.1.3__ * Refactor links slightly * use min extension for css * bump package versions * export more classes __3.1.2__ * Hotfix: fix zooming when canvas not in the top left corner (https://github.com/projectstorm/react-diagrams/pull/88) __3.1.0__ [http://dylanv.blog/2017/09/15/storm-react-diagrams-3-1-0/](https://dylanvorster.com/storm-react-diagrams-3-1-0/) * Zoom relative to mouse location * Fixed links not connecting when grid is larger than port size * Prevented points from dragging when connected to a port and the node itself is not selected * API fixes * Code cleanup __3.0.0__ [http://dylanv.blog/2017/09/13/storm-react-diagrams-v3/](https://dylanvorster.com/storm-react-diagrams-3-0/) * Massive performance updates * Complete rewrite * Started a changelog and design documents for each revision ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing See below for guidelines on house keeping: ### Always add a PR Since the project runs on GitHub, the best way to contribute is to fork and then submit a PR. You will find a template that you will need to fill out ### Adding new demos Add a new folder in the ./demos directory and make sure that it is named correctly like the other demos. A new demo should conform to the standard of either `demo-simple` in which it contains a markdown file that clearly explains 'whats going on', or the code sample should have very clear comments that almost always should ready like an instruction manual such as the simple demo. Finally, you should link up your demo to the __index.tsx__ file in the __demos__ directory. It should be quite self explanatory on how it works, but ultimately I have a helper method that makes it easy to link source code to the demo itself, hence the 'require' statements. The third parameter is if you want to place your demo inside a markdown guide (again: see simple demo for how that is done). ### Make the demo testable Similar procedure, except link your demo in the __index.tsx__ file sitting in the __tests__ directory. Running `yarn run test` will fire up jest (hopefully) and then it will render your demo to a snapshot directory which when run again (for a second time), should compare the output to the newely generated snapshot. Make sure to commit the updated snapshot file with your PR! ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Storm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Introduction [![Join the chat at https://gitter.im/projectstorm/react-diagrams](https://badges.gitter.im/projectstorm/react-diagrams.svg)](https://gitter.im/projectstorm/react-diagrams?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NPM](https://img.shields.io/npm/v/@projectstorm/react-diagrams.svg)](https://npmjs.org/package/@projectstorm/react-diagrams) [![Package Quality](https://npm.packagequality.com/shield/storm-react-diagrams.svg)](https://packagequality.com/#?package=storm-react-diagrams) ![](.gitbook/assets/logo.jpg) **DEMO**: [http://projectstorm.cloud/react-diagrams](http://projectstorm.cloud/react-diagrams) **DOCS \(wip\)** [https://projectstorm.gitbook.io/react-diagrams](https://projectstorm.gitbook.io/react-diagrams) Docs are currently being worked on, along with a migration path. ## What A flow & process orientated diagramming library inspired by **Blender**, **Labview** and **Unreal engine**. * **Modern Codebase** written entirely in Typescript and React, the library makes use of powerful generics, advanced software engineering principles and is broken up into multiple modules. * **Hackable and extensible** the entire library including its core can be extended, rewired and re-assembled into fundamentally different software to suit your own software needs. * **HTML nodes as a first class citizen** the library was originally written to represent advanced dynamic nodes, that are difficult to represent as SVG's due to complex input requirements ux requirements. * **Designed for process** the library is aimed for software engineers that want to rewire their programs at runtime, and that want to make their software more dynamic. * **Fast diagram editing** the defaults provided give the highest priority to editing diagrams as fast as possible. ## Gallery Example implementation using custom models: \(Dylan's personal code\) ![Personal Project](.gitbook/assets/example1.jpg) ![](.gitbook/assets/example2.jpg) Get started with the default models right out of the box: ![](.gitbook/assets/example3.jpg) ## Installing For all the bells and whistles: ```text yarn add @projectstorm/react-diagrams ``` This includes all the packages listed below \(and works \(mostly and conceptually\) like it used to in version 5.0\) ### A more modular approach This library now has a more modular design and you can import just the core \(contains no default factories or routing\) ```text yarn add @projectstorm/react-diagrams-core ``` this is built ontop of the evolving **react-canvas-core** library ```text yarn add @projectstorm/react-canvas-core ``` which makes use of ```text yarn add @projectstorm/geometry ``` and of course, you can add some extras: ```text yarn add @projectstorm/react-diagrams-defaults yarn add @projectstorm/react-diagrams-routing ``` ## How to use Before running any of the examples, please run `pnpm build` in the root. This project is a monorepo, and the packages (including the demos) require the packages to first be built. Take a look at the [diagram demos](https://github.com/projectstorm/react-diagrams/tree/master/diagrams-demo-gallery/demos) **or** Take a look at the [demo project](https://github.com/projectstorm/react-diagrams/tree/master/diagrams-demo-project) which contains an example for ES6 as well as Typescript **or** [Checkout the docs](https://projectstorm.gitbook.io/react-diagrams/) ## Run the demos After running `pnpm install` and `pnpm build`, you must then run: `cd diagrams-demo-gallery && pnpm run start` ## Building from source Simply run `pnpm` then `pnpm build` or `pnpm build:prod` in the root directory and it will spit out the transpiled code and typescript definitions into the dist directory as a single file. ## Built with react-diagrams > Do you have an interesting project built with *react-diagrams*? PR it into this section for others to see. ================================================ FILE: diagrams-demo-gallery/.babelrc.json ================================================ { "sourceType": "unambiguous", "presets": [ [ "@babel/preset-env", { "targets": { "chrome": 100, "safari": 15, "firefox": 91 } } ], "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [] } ================================================ FILE: diagrams-demo-gallery/.storybook/main.js ================================================ module.exports = { stories: ['../demos/*.stories.tsx'], addons: ['@storybook/addon-actions', '@storybook/addon-webpack5-compiler-babel'], framework: '@storybook/react-webpack5' }; ================================================ FILE: diagrams-demo-gallery/.storybook/manager.js ================================================ import { addons } from '@storybook/manager-api'; import diagramsTheme from './theme'; addons.setConfig({ theme: diagramsTheme }); ================================================ FILE: diagrams-demo-gallery/.storybook/preview-head.html ================================================ ================================================ FILE: diagrams-demo-gallery/.storybook/preview.js ================================================ export const parameters = { layout: 'fullscreen' }; ================================================ FILE: diagrams-demo-gallery/.storybook/theme.js ================================================ import { create } from '@storybook/theming'; export default create({ base: 'dark', brandTitle: 'STORM React Diagrams', brandUrl: 'https://github.com/projectstorm/react-diagrams' }); ================================================ FILE: diagrams-demo-gallery/CHANGELOG.md ================================================ # @projectstorm/react-diagrams-gallery ## 7.2.1 ### Patch Changes - adb4415: Fixed the demos ## 7.2.0 ### Minor Changes - 1be4073: Remove setTimeout from demo-dagre to avoid layout breaks ### Patch Changes - 80285fe: refactor: update lodash imports to use individual functions - Updated dependencies [09ed60f] - Updated dependencies [20766f5] - Updated dependencies [80285fe] - @projectstorm/react-canvas-core@7.0.3 - @projectstorm/react-diagrams-core@7.0.3 - @projectstorm/react-diagrams-defaults@7.1.3 - @projectstorm/react-diagrams@7.0.4 ## 7.1.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - Updated dependencies [66c687a] - @projectstorm/react-diagrams-defaults@7.1.2 - @projectstorm/react-diagrams-core@7.0.2 - @projectstorm/react-canvas-core@7.0.2 - @projectstorm/react-diagrams@7.0.3 ## 7.1.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/react-canvas-core@7.0.1 - @projectstorm/react-diagrams@7.0.2 - @projectstorm/react-diagrams-core@7.0.1 - @projectstorm/react-diagrams-defaults@7.1.1 ## 7.1.0 ### Minor Changes - e0d21f1: - [feature] new ability to refresh links in auto distribute system [PR 756](https://github.com/projectstorm/react-diagrams/pull/756) - [fix] Default link now uses the correct method for creating a point allowing this to be overridden [PR 939](https://github.com/projectstorm/react-diagrams/pull/939) Big thanks to @ToTheHit and @h0111in for your help on these, even though its very delayed on my part :) ### Patch Changes - Updated dependencies [e0d21f1] - @projectstorm/react-diagrams-defaults@7.1.0 - @projectstorm/react-diagrams@7.0.1 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-diagrams-defaults@7.0.0 - @projectstorm/react-diagrams-core@7.0.0 - @projectstorm/react-canvas-core@7.0.0 - @projectstorm/react-diagrams@7.0.0 ================================================ FILE: diagrams-demo-gallery/demos/1_SimpleUsage.stories.tsx ================================================ import { Toolkit } from '@projectstorm/react-canvas-core'; Toolkit.TESTING = true; export default { title: 'Simple Usage' }; import demo_simple from './demo-simple'; import demo_flow from './demo-simple-flow'; import demo_performance from './demo-performance'; import demo_locks from './demo-locks'; import demo_grid from './demo-grid'; import demo_listeners from './demo-listeners'; import demo_zoom from './demo-zoom-to-fit'; import demo_zoom_nodes from './demo-zoom-to-fit-nodes'; import demo_canvas_drag from './demo-canvas-drag'; import demo_pan_and_zoom from './demo-pan-and-zoom'; import demo_dynamic_ports from './demo-dynamic-ports'; import demo_labels from './demo-labelled-links'; export const DemoSimple = demo_simple; export const SimpleFlowExample = demo_flow; export const PerformanceDemo = demo_performance; export const LockedWidget = demo_locks; export const CanvasGridSize = demo_grid; export const EventsAndListeners = demo_listeners; export const ZoomToFit = demo_zoom; export const ZoomToFitSelectNodes = demo_zoom_nodes; export const CanvasDrag = demo_canvas_drag; export const CanvasPanAndZoom = demo_pan_and_zoom; export const DynamicPorts = demo_dynamic_ports; export const LinksWithLabels = demo_labels; ================================================ FILE: diagrams-demo-gallery/demos/2_AdvancedUsage.stories.tsx ================================================ import { Toolkit } from '@projectstorm/react-canvas-core'; Toolkit.TESTING = true; export default { title: 'Advanced Usage' }; import demo_adv_clone_selected from './demo-cloning'; import demo_adv_ser_des from './demo-serializing'; import demo_adv_prog from './demo-mutate-graph'; import demo_adv_dnd from './demo-drag-and-drop'; import demo_smart_routing from './demo-smart-routing'; import demo_right_angles_routing from './demo-right-angles-routing'; import demo_alternative_linking from './demo-alternative-linking'; import demo_custom_delete_keys from './demo-custom_delete_keys'; export const CloneSelected = demo_adv_clone_selected; export const SerializingAndDeSerializing = demo_adv_ser_des; export const ProgramaticallyModifyingGraph = demo_adv_prog; export const DragAndDrop = demo_adv_dnd; export const SmartRouting = demo_smart_routing; export const RightAnglesRouting = demo_right_angles_routing; export const LinkingByClickingInsteadOfDragging = demo_alternative_linking; export const SettingCustomDeleteKeys = demo_custom_delete_keys; ================================================ FILE: diagrams-demo-gallery/demos/3_Customization.stories.tsx ================================================ import { Toolkit } from '@projectstorm/react-canvas-core'; Toolkit.TESTING = true; export default { title: 'Customization' }; import demo_custom_link_label from './demo-custom-link-label'; import demo_custom_action from './demo-custom-action'; import demo_cust_nodes from './demo-custom-node1'; import demo_cust_links from './demo-custom-link1'; import demo_cust_links2 from './demo-custom-link2'; export const CustomDiamondNode = demo_cust_nodes; export const CustomAnimatedLinks = demo_cust_links; export const CustomLinkEndsWithArrows = demo_cust_links2; export const CustomLinkLabel = demo_custom_link_label; export const CustomEvent = demo_custom_action; ================================================ FILE: diagrams-demo-gallery/demos/4_Libraries.stories.tsx ================================================ import { Toolkit } from '@projectstorm/react-canvas-core'; Toolkit.TESTING = true; export default { title: 'External Libs' }; import demo_3rd_dagre from './demo-dagre'; import demo_gsap from './demo-animation'; export const DagreDistribute = demo_3rd_dagre; export const GsapAnimation = demo_gsap; ================================================ FILE: diagrams-demo-gallery/demos/demo-alternative-linking/CreateLinkState.ts ================================================ import { Action, ActionEvent, InputType, State } from '@projectstorm/react-canvas-core'; import { PortModel, LinkModel, DiagramEngine } from '@projectstorm/react-diagrams-core'; import { MouseEvent, KeyboardEvent } from 'react'; /** * This state is controlling the creation of a link. */ export class CreateLinkState extends State { sourcePort: PortModel; link: LinkModel; constructor() { super({ name: 'create-new-link' }); this.registerAction( new Action({ type: InputType.MOUSE_UP, fire: (actionEvent: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(actionEvent); const { event: { clientX, clientY } } = actionEvent; const ox = this.engine.getModel().getOffsetX(); const oy = this.engine.getModel().getOffsetY(); if (element instanceof PortModel && !this.sourcePort) { this.sourcePort = element; /* would be cool if link creating could be done somewhat like const link = createLink({ sourcePort: this.sourcePort, points: [{ x: clientX, y: clientY }, { x: clientX, y: clientY }] }) */ const link = this.sourcePort.createLinkModel(); link.setSourcePort(this.sourcePort); link.getFirstPoint().setPosition(clientX - ox, clientY - oy); link.getLastPoint().setPosition(clientX - ox + 20, clientY - oy + 20); this.link = this.engine.getModel().addLink(link); } else if (element instanceof PortModel && this.sourcePort && element != this.sourcePort) { if (this.sourcePort.canLinkToPort(element)) { this.link.setTargetPort(element); element.reportPosition(); this.clearState(); this.eject(); } } else if (element === this.link.getLastPoint()) { this.link.point(clientX - ox, clientY - oy, -1); } this.engine.repaintCanvas(); } }) ); this.registerAction( new Action({ type: InputType.MOUSE_MOVE, fire: (actionEvent: ActionEvent) => { if (!this.link) return; const { event } = actionEvent; this.link.getLastPoint().setPosition(event.clientX, event.clientY); this.engine.repaintCanvas(); } }) ); this.registerAction( new Action({ type: InputType.KEY_UP, fire: (actionEvent: ActionEvent) => { // on esc press remove any started link and pop back to default state if (actionEvent.event.keyCode === 27) { this.link.remove(); this.clearState(); this.eject(); this.engine.repaintCanvas(); } } }) ); } clearState() { this.link = undefined; this.sourcePort = undefined; } } ================================================ FILE: diagrams-demo-gallery/demos/demo-alternative-linking/DefaultState.ts ================================================ import { MouseEvent, TouchEvent } from 'react'; import { SelectingState, State, Action, InputType, ActionEvent, DragCanvasState } from '@projectstorm/react-canvas-core'; import { PortModel, DiagramEngine, DragDiagramItemsState } from '@projectstorm/react-diagrams-core'; import { CreateLinkState } from './CreateLinkState'; export class DefaultState extends State { dragCanvas: DragCanvasState; createLink: CreateLinkState; dragItems: DragDiagramItemsState; constructor() { super({ name: 'starting-state' }); this.childStates = [new SelectingState()]; this.dragCanvas = new DragCanvasState(); this.createLink = new CreateLinkState(); this.dragItems = new DragDiagramItemsState(); // determine what was clicked on this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); // the canvas was clicked on, transition to the dragging canvas state if (!element) { this.transitionWithEvent(this.dragCanvas, event); } // initiate dragging a new link else if (element instanceof PortModel) { return; } // move the items (and potentially link points) else { this.transitionWithEvent(this.dragItems, event); } } }) ); // touch drags the canvas this.registerAction( new Action({ type: InputType.TOUCH_START, fire: (event: ActionEvent) => { this.transitionWithEvent(new DragCanvasState(), event); } }) ); this.registerAction( new Action({ type: InputType.MOUSE_UP, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); if (element instanceof PortModel) this.transitionWithEvent(this.createLink, event); } }) ); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-alternative-linking/index.tsx ================================================ import * as React from 'react'; import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; import { DefaultState } from './DefaultState'; export default () => { const engine = createEngine(); const model = new DiagramModel(); const node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); node1.addOutPort('Out'); node1.setPosition(100, 100); const node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); node2.addInPort('In'); node2.setPosition(400, 100); model.addAll(node1, node2); engine.setModel(model); // Use this custom "DefaultState" instead of the actual default state we get with the engine engine.getStateMachine().pushState(new DefaultState()); return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-animation/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import gsap from 'gsap'; import { DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests the grid size */ class NodeDelayedPosition extends React.Component { constructor(props) { super(props); } render() { const { engine } = this.props; return ( ); } } export default () => { //1) setup the diagram engine var engine = createEngine({ repaintDebounceMs: 12 }); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) create another default node var node3 = new DefaultNodeModel('Node 3', 'rgb(192,255,0)'); node2.setPosition(200, 300); //3-D) create another default node var node4 = new DefaultNodeModel('Node 4', 'rgb(192,255,0)'); node2.setPosition(400, 400); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1, node3, node4); //5) load model into engine engine.setModel(model); var interval = setInterval(() => { [node1, node2, node3, node4].map((node) => { var obj = { x: 0, y: 0 }; gsap.fromTo( obj, { x: node.getPosition().x, y: node.getPosition().y }, { x: Math.floor(Math.random() * 500), y: Math.floor(Math.random() * 500), duration: 0.8, onUpdate: () => { node.setPosition(obj.x, obj.y); engine.repaintCanvas(); } } ); }); }, 2000); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-canvas-drag/index.tsx ================================================ import * as React from 'react'; import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests the drag on/off */ class CanvasDragToggle extends React.Component { enableDrag = () => { const { engine } = this.props; const state = engine.getStateMachine().getCurrentState(); state.dragCanvas.config.allowDrag = true; }; disableDrag = () => { const { engine } = this.props; const state = engine.getStateMachine().getCurrentState(); state.dragCanvas.config.allowDrag = false; }; render() { const { engine } = this.props; return ( Enable canvas drag , Disable canvas drag ]} > ); } } export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-cloning/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, LinkModel, NodeModel } from '@projectstorm/react-diagrams'; import _forEach from 'lodash/forEach'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { BaseModel, CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests cloning */ class CloneSelected extends React.Component { constructor(props: any) { super(props); this.cloneSelected = this.cloneSelected.bind(this); } cloneSelected() { let { engine } = this.props; let offset = { x: 100, y: 100 }; let model = engine.getModel(); let itemMap = {}; _forEach(model.getSelectedEntities(), (item: BaseModel) => { let newItem = item.clone(itemMap); // offset the nodes slightly if (newItem instanceof NodeModel) { newItem.setPosition(newItem.getX() + offset.x, newItem.getY() + offset.y); model.addNode(newItem); } else if (newItem instanceof LinkModel) { // offset the link points newItem.getPoints().forEach((p) => { p.setPosition(p.getX() + offset.x, p.getY() + offset.y); }); model.addLink(newItem); } (newItem as BaseModel).setSelected(false); }); this.forceUpdate(); } render() { const { engine } = this.props; return ( Clone Selected}> ); } } export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); let port = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); let port2 = node2.addInPort('In'); node2.setPosition(400, 100); // link the ports let link1 = port.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-action/index.tsx ================================================ import * as React from 'react'; import _forEach from 'lodash/forEach'; import createEngine, { DiagramModel, DefaultNodeModel, DefaultLinkModel } from '@projectstorm/react-diagrams'; import { CanvasWidget, Action, ActionEvent, InputType } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; interface CustomDeleteItemsActionOptions { keyCodes?: number[]; } /** * Deletes all selected items, but asks for confirmation first */ class CustomDeleteItemsAction extends Action { constructor(options: CustomDeleteItemsActionOptions = {}) { options = { keyCodes: [46, 8], ...options }; super({ type: InputType.KEY_DOWN, fire: (event: ActionEvent) => { if (options.keyCodes.indexOf(event.event.keyCode) !== -1) { const selectedEntities = this.engine.getModel().getSelectedEntities(); if (selectedEntities.length > 0) { const confirm = window.confirm('Are you sure you want to delete?'); if (confirm) { _forEach(selectedEntities, (model) => { // only delete items which are not locked if (!model.isLocked()) { model.remove(); } }); this.engine.repaintCanvas(); } } } } }); } } export default () => { // create an engine without registering DeleteItemsAction const engine = createEngine({ registerDefaultDeleteItemsAction: false }); const model = new DiagramModel(); const node1 = new DefaultNodeModel({ name: 'Node 1', color: 'rgb(0,192,255)' }); node1.setPosition(100, 100); const port1 = node1.addOutPort('Out'); const node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); const port2 = node2.addInPort('In'); node2.setPosition(400, 100); const link1 = port1.link(port2); link1.getOptions().testName = 'Test'; link1.addLabel('Hello World!'); model.addAll(node1, node2, link1); engine.setModel(model); // register an DeleteItemsAction with custom keyCodes (in this case, only Delete key) engine.getActionEventBus().registerAction(new CustomDeleteItemsAction()); return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link-label/EditableLabelFactory.tsx ================================================ import * as React from 'react'; import { AbstractReactFactory, GenerateWidgetEvent } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams'; import { EditableLabelModel } from './EditableLabelModel'; import { EditableLabelWidget } from './EditableLabelWidget'; import { JSX } from 'react'; export class EditableLabelFactory extends AbstractReactFactory { constructor() { super('editable-label'); } generateModel(): EditableLabelModel { return new EditableLabelModel(); } generateReactWidget(event: GenerateWidgetEvent): JSX.Element { return ; } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link-label/EditableLabelModel.ts ================================================ import { LabelModel } from '@projectstorm/react-diagrams'; import { BaseModelOptions, DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface EditableLabelOptions extends BaseModelOptions { value?: string; } export class EditableLabelModel extends LabelModel { value: string; constructor(options: EditableLabelOptions = {}) { super({ ...options, type: 'editable-label' }); this.value = options.value || ''; } serialize() { return { ...super.serialize(), value: this.value }; } deserialize(event: DeserializeEvent): void { super.deserialize(event); this.value = event.data.value; } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link-label/EditableLabelWidget.tsx ================================================ import * as React from 'react'; import { EditableLabelModel } from './EditableLabelModel'; import styled from '@emotion/styled'; import { action } from '@storybook/addon-actions'; export interface FlowAliasLabelWidgetProps { model: EditableLabelModel; } namespace S { // NOTE: this CSS rules allows to interact with elements in label export const Label = styled.div` user-select: none; pointer-events: auto; `; } // now we can render all what we want in the label export const EditableLabelWidget: React.FunctionComponent = (props) => { const [str, setStr] = React.useState(props.model.value); return ( { const newVal = event.target.value; // update value both in internal component state setStr(newVal); // and in model object props.model.value = newVal; }} /> ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link-label/index.tsx ================================================ import * as React from 'react'; import createEngine, { DefaultNodeModel, DiagramModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; import { EditableLabelFactory } from './EditableLabelFactory'; import { EditableLabelModel } from './EditableLabelModel'; /** * @Author Shumaf Lovpache (aka Soarex16) */ export default () => { // engine setup const engine = createEngine(); // register our label factory engine.getLabelFactories().registerFactory(new EditableLabelFactory()); // setup diagram model const model = new DiagramModel(); // create some nodes const node1 = new DefaultNodeModel('Node1', 'red'); const port1 = node1.addOutPort('out'); node1.setPosition(250, 100); const node2 = new DefaultNodeModel('Node2', 'green'); const port2 = node2.addInPort('in'); node2.setPosition(800, 300); // link nodes together const link1 = port1.link(port2); // !!! // add our custom label to link link1.addLabel( new EditableLabelModel({ value: 'Hello, I am label!' }) ); // add models to the root graph model.addAll(node1, port1, node2, port2, link1); // load model into engine engine.setModel(model); // render diagram return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link1/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, DefaultLinkFactory, DefaultLinkModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export class AdvancedLinkModel extends DefaultLinkModel { constructor() { super({ type: 'advanced', width: 10 }); } } export class AdvancedPortModel extends DefaultPortModel { createLinkModel(): AdvancedLinkModel | null { return new AdvancedLinkModel(); } } export class AdvancedLinkSegment extends React.Component<{ model: AdvancedLinkModel; path: string }> { path: SVGPathElement; circle: SVGCircleElement; callback: () => any; percent: number; handle: any; mounted: boolean; constructor(props) { super(props); this.percent = 0; } componentDidMount() { this.mounted = true; this.callback = () => { if (!this.circle || !this.path) { return; } this.percent += 2; if (this.percent > 100) { this.percent = 0; } let point = this.path.getPointAtLength(this.path.getTotalLength() * (this.percent / 100.0)); this.circle.setAttribute('cx', '' + point.x); this.circle.setAttribute('cy', '' + point.y); if (this.mounted) { requestAnimationFrame(this.callback); } }; requestAnimationFrame(this.callback); } componentWillUnmount() { this.mounted = false; } render() { return ( <> { this.path = ref; }} strokeWidth={this.props.model.getOptions().width} stroke="rgba(255,0,0,0.5)" d={this.props.path} /> { this.circle = ref; }} r={10} fill="orange" /> ); } } export class AdvancedLinkFactory extends DefaultLinkFactory { constructor() { super('advanced'); } generateModel(): AdvancedLinkModel { return new AdvancedLinkModel(); } generateLinkSegment(model: AdvancedLinkModel, selected: boolean, path: string) { return ( ); } } /** * * Simple link styling demo * * @Author kfrajtak */ export default () => { //1) setup the diagram engine var engine = createEngine(); engine.getLinkFactories().registerFactory(new AdvancedLinkFactory()); // create some nodes var node1 = new DefaultNodeModel('Source', 'rgb(0,192,255)'); let port1 = node1.addPort(new AdvancedPortModel(false, 'out-1', 'Out thick')); let port2 = node1.addPort(new DefaultPortModel(false, 'out-2', 'Out default')); node1.setPosition(100, 100); var node2 = new DefaultNodeModel('Target', 'rgb(192,255,0)'); var port3 = node2.addPort(new AdvancedPortModel(true, 'in-1', 'In thick')); var port4 = node2.addPort(new DefaultPortModel(true, 'in-2', 'In default')); node2.setPosition(300, 100); var node3 = new DefaultNodeModel('Source', 'rgb(0,192,255)'); node3.addPort(new AdvancedPortModel(false, 'out-1', 'Out thick')); node3.addPort(new DefaultPortModel(false, 'out-2', 'Out default')); node3.setPosition(100, 200); var node4 = new DefaultNodeModel('Target', 'rgb(192,255,0)'); node4.addPort(new AdvancedPortModel(true, 'in-1', 'In thick')); node4.addPort(new DefaultPortModel(true, 'in-2', 'In default')); node4.setPosition(300, 200); var model = new DiagramModel(); model.addAll(port1.link(port3), port2.link(port4)); // add everything else model.addAll(node1, node2, node3, node4); // load model into engine engine.setModel(model); // render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-link2/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, DefaultLinkFactory, DefaultLinkModel, DefaultLinkWidget } from '@projectstorm/react-diagrams'; import { LinkWidget, PointModel } from '@projectstorm/react-diagrams-core'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; import { JSX, MouseEvent } from 'react'; import { DefaultLinkPointWidget, DefaultLinkSegmentWidget } from '@projectstorm/react-diagrams-defaults/dist'; import { DiagramEngine } from '@projectstorm/react-diagrams-core/dist'; export class AdvancedLinkModel extends DefaultLinkModel { constructor() { super({ type: 'advanced', width: 4 }); } } export class AdvancedPortModel extends DefaultPortModel { createLinkModel(): AdvancedLinkModel | null { return new AdvancedLinkModel(); } } const CustomLinkArrowWidget = (props) => { const { point, previousPoint } = props; const angle = 90 + (Math.atan2( point.getPosition().y - previousPoint.getPosition().y, point.getPosition().x - previousPoint.getPosition().x ) * 180) / Math.PI; //translate(50, -10), return ( ); }; export interface AdvancedLinkWWidgetProps { link: DefaultLinkModel; diagramEngine: DiagramEngine; pointAdded?: (point: PointModel, event: MouseEvent) => any; renderPoints?: boolean; selected?: (event: MouseEvent) => any; } export class AdvancedLinkWidget extends React.Component { generatePoint = (point: PointModel): JSX.Element => { return ( ); }; generateLink = (path: string, extraProps: any, id: string | number): JSX.Element => { return ( ); }; addPointToLink = (event: MouseEvent, index: number) => { if ( !event.shiftKey && !this.props.link.isLocked() && this.props.link.getPoints().length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink() ) { const position = this.props.diagramEngine.getRelativeMousePoint(event); const point = this.props.link.point(position.x, position.y, index); event.persist(); event.stopPropagation(); this.props.diagramEngine.getActionEventBus().fireAction({ event, model: point }); } }; generateArrow(point: PointModel, previousPoint: PointModel): JSX.Element { return ( ); } render() { //ensure id is present for all points on the path var points = this.props.link.getPoints(); var paths = []; //draw the multiple anchors and complex line instead for (let j = 0; j < points.length - 1; j++) { paths.push( this.generateLink( LinkWidget.generateLinePath(points[j], points[j + 1]), { 'data-linkid': this.props.link.getID(), 'data-point': j, onMouseDown: (event: MouseEvent) => { this.addPointToLink(event, j + 1); } }, j ) ); } //render the circles for (let i = 1; i < points.length - 1; i++) { paths.push(this.generatePoint(points[i])); } if (this.props.link.getTargetPort() !== null) { paths.push(this.generateArrow(points[points.length - 1], points[points.length - 2])); } else { paths.push(this.generatePoint(points[points.length - 1])); } return {paths}; } } export class AdvancedLinkFactory extends DefaultLinkFactory { constructor() { super('advanced'); } generateModel(): AdvancedLinkModel { return new AdvancedLinkModel(); } generateReactWidget(event): JSX.Element { return ; } } /** * * Simple link styling demo * * @Author kfrajtak */ export default () => { //1) setup the diagram engine var engine = createEngine(); engine.getLinkFactories().registerFactory(new AdvancedLinkFactory()); // create some nodes var node1 = new DefaultNodeModel('Source', 'rgb(0,192,255)'); let port1 = node1.addPort(new AdvancedPortModel(false, 'out')); node1.setPosition(100, 100); var node2 = new DefaultNodeModel('Target', 'rgb(192,255,0)'); var port2 = node2.addPort(new AdvancedPortModel(true, 'in')); node2.setPosition(500, 350); var node3 = new DefaultNodeModel('Source', 'rgb(0,192,255)'); let port3 = node3.addPort(new AdvancedPortModel(false, 'out')); node3.setPosition(100, 500); var node4 = new DefaultNodeModel('Target', 'rgb(192,255,0)'); var port4 = node4.addPort(new AdvancedPortModel(true, 'in')); node4.setPosition(500, 450); var model = new DiagramModel(); model.addAll(port1.link(port2), port3.link(port4)); // add everything else model.addAll(node1, node2, node3, node4); // load model into engine engine.setModel(model); // render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/DiamondNodeFactory.tsx ================================================ import { DiamondNodeWidget } from './DiamondNodeWidget'; import { DiamondNodeModel } from './DiamondNodeModel'; import * as React from 'react'; import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { JSX } from 'react'; export class DiamondNodeFactory extends AbstractReactFactory { constructor() { super('diamond'); } generateReactWidget(event): JSX.Element { return ; } generateModel(event) { return new DiamondNodeModel(); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/DiamondNodeModel.ts ================================================ import { NodeModel, NodeModelGenerics, PortModelAlignment } from '@projectstorm/react-diagrams'; import { DiamondPortModel } from './DiamondPortModel'; export interface DiamondNodeModelGenerics { PORT: DiamondPortModel; } export class DiamondNodeModel extends NodeModel { constructor() { super({ type: 'diamond' }); this.addPort(new DiamondPortModel(PortModelAlignment.TOP)); this.addPort(new DiamondPortModel(PortModelAlignment.LEFT)); this.addPort(new DiamondPortModel(PortModelAlignment.BOTTOM)); this.addPort(new DiamondPortModel(PortModelAlignment.RIGHT)); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/DiamondNodeWidget.tsx ================================================ import * as React from 'react'; import { DiamondNodeModel } from './DiamondNodeModel'; import { DiagramEngine, PortModelAlignment, PortWidget } from '@projectstorm/react-diagrams'; import styled from '@emotion/styled'; export interface DiamondNodeWidgetProps { node: DiamondNodeModel; engine: DiagramEngine; size?: number; } namespace S { export const Port = styled.div` width: 16px; height: 16px; z-index: 10; background: rgba(0, 0, 0, 0.5); border-radius: 8px; cursor: pointer; &:hover { background: rgba(0, 0, 0, 1); } `; } /** * @author Dylan Vorster */ export class DiamondNodeWidget extends React.Component { render() { return (
` }} />
); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/DiamondPortModel.ts ================================================ import { LinkModel, PortModel, DefaultLinkModel, PortModelAlignment } from '@projectstorm/react-diagrams'; export class DiamondPortModel extends PortModel { constructor(alignment: PortModelAlignment) { super({ type: 'diamond', name: alignment, alignment: alignment }); } createLinkModel(): LinkModel { return new DefaultLinkModel(); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/SimplePortFactory.ts ================================================ import { DiagramEngine, PortModel } from '@projectstorm/react-diagrams'; import { AbstractModelFactory } from '@projectstorm/react-canvas-core'; export class SimplePortFactory extends AbstractModelFactory { cb: (initialConfig?: any) => PortModel; constructor(type: string, cb: (initialConfig?: any) => PortModel) { super(type); this.cb = cb; } generateModel(event): PortModel { return this.cb(event.initialConfig); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-custom-node1/index.tsx ================================================ import createEngine, { DefaultNodeModel, DiagramModel, PortModelAlignment } from '@projectstorm/react-diagrams'; import * as React from 'react'; // import the custom models import { DiamondNodeModel } from './DiamondNodeModel'; import { DiamondNodeFactory } from './DiamondNodeFactory'; import { SimplePortFactory } from './SimplePortFactory'; import { DiamondPortModel } from './DiamondPortModel'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * @Author Dylan Vorster */ export default () => { //1) setup the diagram engine var engine = createEngine(); // register some other factories as well engine .getPortFactories() .registerFactory(new SimplePortFactory('diamond', (config) => new DiamondPortModel(PortModelAlignment.LEFT))); engine.getNodeFactories().registerFactory(new DiamondNodeFactory()); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 200); //3-B) create our new custom node var node2 = new DiamondNodeModel(); node2.setPosition(250, 108); var node3 = new DefaultNodeModel('Node 3', 'red'); var port3 = node3.addInPort('In'); node3.setPosition(500, 100); //3-C) link the 2 nodes together var link1 = port1.link(node2.getPort(PortModelAlignment.LEFT)); var link2 = port3.link(node2.getPort(PortModelAlignment.RIGHT)); var node4 = new DefaultNodeModel('Node 4', 'rgb(0,192,255)'); var port4 = node4.addOutPort('Out'); node4.setPosition(200, 10); var link3 = port4.link(node2.getPort(PortModelAlignment.TOP)); var node5 = new DefaultNodeModel('Node 5', 'mediumpurple'); var port5 = node5.addInPort('In'); node5.setPosition(400, 300); var link4 = port5.link(node2.getPort(PortModelAlignment.BOTTOM)); //4) add the models to the root graph model.addAll(node1, node2, node3, link1, link2, node4, link3, link4, node5); //5) load model into engine engine.setModel(model); //6) render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-custom_delete_keys/index.tsx ================================================ import * as React from 'react'; import createEngine, { DiagramModel, DefaultNodeModel, DefaultLinkModel } from '@projectstorm/react-diagrams'; import { CanvasWidget, DeleteItemsAction } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { // create an engine without registering DeleteItemsAction const engine = createEngine({ registerDefaultDeleteItemsAction: false }); const model = new DiagramModel(); const node1 = new DefaultNodeModel({ name: 'Node 1', color: 'rgb(0,192,255)' }); node1.setPosition(100, 100); const port1 = node1.addOutPort('Out'); const node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); const port2 = node2.addInPort('In'); node2.setPosition(400, 100); const link1 = port1.link(port2); link1.getOptions().testName = 'Test'; link1.addLabel('Hello World!'); model.addAll(node1, node2, link1); engine.setModel(model); // register an DeleteItemsAction with custom keyCodes (in this case, only Delete key) engine.getActionEventBus().registerAction(new DeleteItemsAction({ keyCodes: [46] })); return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-dagre/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, NodeModel, DagreEngine, DiagramEngine, PathFindingLinkFactory } from '@projectstorm/react-diagrams'; import { useLayoutEffect, useRef } from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; function createNode(name): any { return new DefaultNodeModel(name, 'rgb(0,192,255)'); } let count = 0; function connectNodes(nodeFrom, nodeTo, engine: DiagramEngine) { //just to get id-like structure count++; const portOut = nodeFrom.addPort(new DefaultPortModel(true, `${nodeFrom.name}-out-${count}`, 'Out')); const portTo = nodeTo.addPort(new DefaultPortModel(false, `${nodeFrom.name}-to-${count}`, 'IN')); return portOut.link(portTo); // ################# UNCOMMENT THIS LINE FOR PATH FINDING ############################# // return portOut.link(portTo, engine.getLinkFactories().getFactory(PathFindingLinkFactory.NAME)); // ##################################################################################### } /** * Tests auto distribution */ function genDagreEngine() { return new DagreEngine({ graph: { rankdir: 'RL', ranker: 'longest-path', marginx: 25, marginy: 25 }, includeLinks: true, nodeMargin: 25 }); } function autoDistribute(engine: DiagramEngine) { const model = engine.getModel(); const dagreEngine = genDagreEngine(); dagreEngine.redistribute(model); reroute(engine); engine.repaintCanvas(); } function autoRefreshLinks(engine: DiagramEngine) { const model = engine.getModel(); const dagreEngine = genDagreEngine(); dagreEngine.refreshLinks(model); // only happens if pathfing is enabled (check line 29) reroute(engine); engine.repaintCanvas(); } function reroute(engine: DiagramEngine) { engine.getLinkFactories().getFactory(PathFindingLinkFactory.NAME).calculateRoutingMatrix(); } function DemoWidget(props) { const engine = props.engine; useLayoutEffect(() => { autoDistribute(engine); }, []); const redistribute = () => { autoDistribute(engine); }; const refreshLinks = () => { autoRefreshLinks(engine); }; return ( Re-distribute Refresh Links } > ); } export default () => { //1) setup the diagram engine const engineRef = useRef(createEngine()); let engine = engineRef.current; //2) setup the diagram model let model = new DiagramModel(); //3) create a default nodes let nodesFrom: NodeModel[] = []; let nodesTo: NodeModel[] = []; nodesFrom.push(createNode('from-1')); nodesFrom.push(createNode('from-2')); nodesFrom.push(createNode('from-3')); nodesTo.push(createNode('to-1')); nodesTo.push(createNode('to-2')); nodesTo.push(createNode('to-3')); //4) link nodes together let links = nodesFrom.map((node, index) => { return connectNodes(node, nodesTo[index], engine); }); // more links for more complicated diagram links.push(connectNodes(nodesTo[0], nodesTo[1], engine)); links.push(connectNodes(nodesTo[1], nodesTo[2], engine)); links.push(connectNodes(nodesTo[0], nodesTo[2], engine)); links.push(connectNodes(nodesFrom[0], nodesFrom[2], engine)); links.push(connectNodes(nodesFrom[0], nodesTo[2], engine)); // initial random position nodesFrom.forEach((node, index) => { node.setPosition(index * 70, index * 70); model.addNode(node); }); nodesTo.forEach((node, index) => { node.setPosition(index * 70, 100); model.addNode(node); }); links.forEach((link) => { model.addLink(link); }); engine.setModel(model); return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-drag-and-drop/Application.ts ================================================ import * as SRD from '@projectstorm/react-diagrams'; /** * @author Dylan Vorster */ export class Application { protected activeModel: SRD.DiagramModel; protected diagramEngine: SRD.DiagramEngine; constructor() { this.diagramEngine = SRD.default(); this.newModel(); } public newModel() { this.activeModel = new SRD.DiagramModel(); this.diagramEngine.setModel(this.activeModel); //3-A) create a default node var node1 = new SRD.DefaultNodeModel('Node 1', 'rgb(0,192,255)'); let port = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new SRD.DefaultNodeModel('Node 2', 'rgb(192,255,0)'); let port2 = node2.addInPort('In'); node2.setPosition(400, 100); // link the ports let link1 = port.link(port2); this.activeModel.addAll(node1, node2, link1); } public getActiveDiagram(): SRD.DiagramModel { return this.activeModel; } public getDiagramEngine(): SRD.DiagramEngine { return this.diagramEngine; } } ================================================ FILE: diagrams-demo-gallery/demos/demo-drag-and-drop/components/BodyWidget.tsx ================================================ import * as React from 'react'; import _keys from 'lodash/keys'; import { TrayWidget } from './TrayWidget'; import { Application } from '../Application'; import { TrayItemWidget } from './TrayItemWidget'; import { DefaultNodeModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../../helpers/DemoCanvasWidget'; import styled from '@emotion/styled'; export interface BodyWidgetProps { app: Application; } namespace S { export const Body = styled.div` flex-grow: 1; display: flex; flex-direction: column; min-height: 100%; `; export const Header = styled.div` display: flex; background: rgb(30, 30, 30); flex-grow: 0; flex-shrink: 0; color: white; font-family: Helvetica, Arial, sans-serif; padding: 10px; align-items: center; `; export const Content = styled.div` display: flex; flex-grow: 1; `; export const Layer = styled.div` position: relative; flex-grow: 1; `; } export class BodyWidget extends React.Component { render() { return (
Storm React Diagrams - DnD demo
{ var data = JSON.parse(event.dataTransfer.getData('storm-diagram-node')); var nodesCount = _keys(this.props.app.getDiagramEngine().getModel().getNodes()).length; var node: DefaultNodeModel = null; if (data.type === 'in') { node = new DefaultNodeModel('Node ' + (nodesCount + 1), 'rgb(192,255,0)'); node.addInPort('In'); } else { node = new DefaultNodeModel('Node ' + (nodesCount + 1), 'rgb(0,192,255)'); node.addOutPort('Out'); } var point = this.props.app.getDiagramEngine().getRelativeMousePoint(event); node.setPosition(point); this.props.app.getDiagramEngine().getModel().addNode(node); this.forceUpdate(); }} onDragOver={(event) => { event.preventDefault(); }} >
); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-drag-and-drop/components/TrayItemWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; export interface TrayItemWidgetProps { model: any; color?: string; name: string; } namespace S { export const Tray = styled.div<{ color: string }>` color: white; font-family: Helvetica, Arial; padding: 5px; margin: 0px 10px; border: solid 1px ${(p) => p.color}; border-radius: 5px; margin-bottom: 2px; cursor: pointer; `; } export class TrayItemWidget extends React.Component { render() { return ( { event.dataTransfer.setData('storm-diagram-node', JSON.stringify(this.props.model)); }} className="tray-item" > {this.props.name} ); } } ================================================ FILE: diagrams-demo-gallery/demos/demo-drag-and-drop/components/TrayWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; namespace S { export const Tray = styled.div` min-width: 200px; background: rgb(20, 20, 20); flex-grow: 0; flex-shrink: 0; `; } export class TrayWidget extends React.Component { render() { return {this.props.children}; } } ================================================ FILE: diagrams-demo-gallery/demos/demo-drag-and-drop/index.tsx ================================================ import * as React from 'react'; import { BodyWidget } from './components/BodyWidget'; import { Application } from './Application'; export default () => { var app = new Application(); return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-dynamic-ports/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DiagramEngine } from '@projectstorm/react-diagrams'; import _values from 'lodash/values'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; class CloneSelected extends React.Component<{ model: DiagramModel; engine: DiagramEngine }, any> { addPorts = () => { const nodes: DefaultNodeModel[] = _values(this.props.model.getNodes()) as DefaultNodeModel[]; for (let node of nodes) { if (node.getOptions().name === 'Node 2') { node.addInPort(`in-${node.getInPorts().length + 1}`, false); } else { node.addOutPort(`out-${node.getOutPorts().length + 1}`, false); } } this.props.engine.repaintCanvas(); }; render() { const { engine } = this.props; return ( Add more ports}> ); } } export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); node2.setPosition(400, 100); // link the ports //4) add the models to the root graph model.addAll(node1, node2); //5) load model into engine engine.setModel(model); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-grid/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests the grid size */ export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); model.setGridSize(50); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); let port = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); let port2 = node2.addInPort('In'); node2.setPosition(400, 100); // link the ports let link1 = port.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-labelled-links/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultLinkModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { action } from '@storybook/addon-actions'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { // setup the diagram engine const engine = createEngine(); // setup the diagram model const model = new DiagramModel(); // create four nodes const node1 = new DefaultNodeModel('Node A', 'rgb(0,192,255)'); const port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); const node2 = new DefaultNodeModel('Node B', 'rgb(255,255,0)'); const port2 = node2.addInPort('In'); node2.setPosition(400, 50); const node3 = new DefaultNodeModel('Node C (no label)', 'rgb(192,255,255)'); const port3 = node3.addInPort('In'); node3.setPosition(450, 180); const node4 = new DefaultNodeModel('Node D', 'rgb(192,0,255)'); const port4 = node4.addInPort('In'); node4.setPosition(300, 250); // link node A and B together and give it a label const link1 = port1.link(port2); (link1 as DefaultLinkModel).addLabel('Custom label 1'); (link1 as DefaultLinkModel).addLabel('Custom label 2'); // no label for A and C, just a link const link2 = port1.link(port3); // also a label for A and D const link3 = port1.link(port4); (link3 as DefaultLinkModel).addLabel('Emoji label: 🎉'); // add all to the main model model.addAll(node1, node2, node3, node4, link1, link2, link3); // load model into engine and render engine.setModel(model); return ( { action('Serialized Graph')(JSON.stringify(model.serializeDiagram(), null, 2)); }} > Serialize Graph } > ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-listeners/index.tsx ================================================ import * as React from 'react'; import { action } from '@storybook/addon-actions'; import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Shows some of the events triggered when elements are selected */ export default () => { // setup the diagram engine var engine = createEngine(); var model = new DiagramModel(); // sample for link with simple line var node1 = new DefaultNodeModel('Node 1', 'rgb(255,99,66)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 40); var node3 = new DefaultNodeModel('Node 3', 'rgb(128,99,255)'); var port3 = node3.addInPort('In'); node3.setPosition(300, 160); //link the nodes let link1 = port1.link(port2); let link2 = port1.link(port3); // add all the models let models = model.addAll(node1, node2, node3, link1, link2); // add a selection listener to each models.forEach((item) => { item.registerListener({ eventDidFire: action('element eventDidFire') }); }); model.registerListener({ eventDidFire: action('model eventDidFire') }); engine.setModel(model); return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-locks/index.tsx ================================================ import * as React from 'react'; import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * * Shows how you can lock down the system so that the entire scene cant be interacted with. * * @Author Dylan Vorster */ export default () => { //1) setup the diagram engine var engine = createEngine(); var model = new DiagramModel(); // sample for link with simple line (no additional points) var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); let link1 = port1.link(port2); model.addAll(node1, node2, link1); // sample for link with complex line (additional points) var node3 = new DefaultNodeModel('Node 3', 'rgb(0,192,255)'); var port3 = node3.addOutPort('Out'); node3.setPosition(100, 250); var node4 = new DefaultNodeModel('Node 4', 'rgb(192,255,0)'); var port4 = node4.addInPort('In'); node4.setPosition(400, 250); var link2 = port3.link(port4); link2.point(350, 225); link2.point(200, 225); model.addAll(node3, node4, link2); engine.setModel(model); //!========================================= <<<<<<< model.setLocked(true); //!========================================= <<<<<<< return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-mutate-graph/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, NodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import _values from 'lodash/values'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests the grid size */ class NodeDelayedPosition extends React.Component { constructor(props) { super(props); this.updatePosition = this.updatePosition.bind(this); this.updatePositionViaSerialize = this.updatePositionViaSerialize.bind(this); } updatePosition() { const { engine } = this.props; let model = engine.getModel(); const nodes = model.getNodes(); let node = nodes[Object.keys(nodes)[0]]; node.setPosition(node.getX() + 30, node.getY() + 30); engine.repaintCanvas(); } updatePositionViaSerialize() { let { engine } = this.props; let model = engine.getModel(); let str = JSON.stringify(model.serialize()); let model2 = new DiagramModel(); let obj: ReturnType = JSON.parse(str); let node: ReturnType = _values(obj.layers[1].models)[0] as any; node.x += 30; node.y += 30; model2.deserializeModel(obj, engine); engine.setModel(model2); } render() { const { engine } = this.props; return ( Update position , Update position via serialize ]} > ); } } export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-pan-and-zoom/index.tsx ================================================ import * as React from 'react'; import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * Tests the pan and zoom action, which is intended as a trackpad/mobile * alternative to the standard ZoomCanvasAction */ class CanvasPanAndZoomToggle extends React.Component { render() { const { engine } = this.props; return ( ); } } export default () => { /** * 1) setup the diagram engine * PandAndZoomCanvasAction and ZoomCanvasAction are mutually exclusive * If both are enabled, ZoomCanvasAction will override. */ var engine = createEngine({ registerDefaultPanAndZoomCanvasAction: true, registerDefaultZoomCanvasAction: false }); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ; }; ================================================ FILE: diagrams-demo-gallery/demos/demo-performance/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * * Simple stress test of the system, shows that it can handle many nodes, and * retain good performance * * @Author Dylan Vorster */ export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { generateNodes(model, i * 200, j * 100); } } //5) load model into engine engine.setModel(model); //6) render the diagram! return ( ); }; function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) { //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100 + offsetX, 100 + offsetY); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(200 + offsetX, 100 + offsetY); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); } ================================================ FILE: diagrams-demo-gallery/demos/demo-right-angles-routing/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, RightAngleLinkFactory, LinkModel, RightAngleLinkModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { action } from '@storybook/addon-actions'; import { AbstractModelFactory, CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; // When new link is created by clicking on port the RightAngleLinkModel needs to be returned. export class RightAnglePortModel extends DefaultPortModel { createLinkModel(factory?: AbstractModelFactory) { return new RightAngleLinkModel(); } } export default () => { // setup the diagram engine const engine = createEngine(); engine.getLinkFactories().registerFactory(new RightAngleLinkFactory()); // setup the diagram model const model = new DiagramModel(); // create four nodes in a way that straight links wouldn't work const node1 = new DefaultNodeModel('Node A', 'rgb(0,192,255)'); const port1 = node1.addPort(new RightAnglePortModel(false, 'out-1', 'Out')); node1.setPosition(340, 350); const node2 = new DefaultNodeModel('Node B', 'rgb(255,255,0)'); const port2 = node2.addPort(new RightAnglePortModel(false, 'out-1', 'Out')); node2.setPosition(240, 80); const node3 = new DefaultNodeModel('Node C', 'rgb(192,255,255)'); const port3 = node3.addPort(new RightAnglePortModel(true, 'in-1', 'In')); node3.setPosition(540, 180); const node4 = new DefaultNodeModel('Node D', 'rgb(192,0,255)'); const port4 = node4.addPort(new RightAnglePortModel(true, 'in-1', 'In')); node4.setPosition(95, 185); // linking things together const link1 = port1.link(port4); const link2 = port2.link(port3); // add all to the main model model.addAll(node1, node2, node3, node4, link1, link2); // load model into engine and render engine.setModel(model); return ( { action('Serialized Graph')(JSON.stringify(model.serialize(), null, 2)); }} > Serialize Graph } > ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-serializing/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultLabelModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { action } from '@storybook/addon-actions'; import * as beautify from 'json-beautify'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) link the 2 nodes together var link1 = port1.link(port2); link1.addLabel(new DefaultLabelModel({ label: 'Label' })); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //!------------- SERIALIZING ------------------ var str = JSON.stringify(model.serialize()); //!------------- DESERIALIZING ---------------- var model2 = new DiagramModel(); model2.deserializeModel(JSON.parse(str), engine); engine.setModel(model2); return ( { action('Serialized Graph')(beautify(model2.serialize(), null, 2, 80)); }} > Serialize Graph } > ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-simple/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultLinkModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel({ name: 'Node 1', color: 'rgb(0,192,255)' }); node1.setPosition(100, 100); let port1 = node1.addOutPort('Out'); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); let port2 = node2.addInPort('In'); node2.setPosition(400, 100); // link the ports let link1 = port1.link(port2); link1.getOptions().testName = 'Test'; link1.addLabel('Hello World!'); //4) add the models to the root graph model.addAll(node1, node2, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-simple-flow/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultDiagramState } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { //1) setup the diagram engine var engine = createEngine(); // ############################################ MAGIC HAPPENS HERE const state = engine.getStateMachine().getCurrentState(); if (state instanceof DefaultDiagramState) { state.dragNewLink.config.allowLooseLinks = false; } // ############################################ MAGIC HAPPENS HERE //2) setup the diagram model var model = new DiagramModel(); //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100, 100); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(400, 100); //3-C) link the 2 nodes together var link1 = port1.link(port2); //3-D) create an orphaned node var node3 = new DefaultNodeModel('Node 3', 'rgb(0,192,255)'); node3.addOutPort('Out'); node3.setPosition(100, 200); //4) add the models to the root graph model.addAll(node1, node2, node3, link1); //5) load model into engine engine.setModel(model); //6) render the diagram! return ( ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-smart-routing/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel, DefaultPortModel, PathFindingLinkFactory, DefaultLabelModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoButton, DemoWorkspaceWidget } from '../helpers/DemoWorkspaceWidget'; import { action } from '@storybook/addon-actions'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; export default () => { // setup the diagram engine const engine = createEngine(); // setup the diagram model const model = new DiagramModel(); // create four nodes in a way that straight links wouldn't work const node1 = new DefaultNodeModel('Node A', 'rgb(0,192,255)'); const port1 = node1.addPort(new DefaultPortModel(false, 'out-1', 'Out')); node1.setPosition(340, 350); const node2 = new DefaultNodeModel('Node B', 'rgb(255,255,0)'); const port2 = node2.addPort(new DefaultPortModel(false, 'out-1', 'Out')); node2.setPosition(240, 80); const node3 = new DefaultNodeModel('Node C', 'rgb(192,255,255)'); const port3 = node3.addPort(new DefaultPortModel(true, 'in-1', 'In')); node3.setPosition(540, 180); const node4 = new DefaultNodeModel('Node D', 'rgb(192,0,255)'); const port4 = node4.addPort(new DefaultPortModel(true, 'in-1', 'In')); node4.setPosition(95, 185); const node5 = new DefaultNodeModel('Node E', 'rgb(192,255,0)'); node5.setPosition(250, 180); const pathfinding = engine.getLinkFactories().getFactory(PathFindingLinkFactory.NAME); // linking things together (specifically using the pathfinding link) const link1 = port1.link(port4, pathfinding); const link2 = port2.link(port3, pathfinding); link1.addLabel( new DefaultLabelModel({ label: 'I am a label!', offsetY: 20 }) ); // add all to the main model model.addAll(node1, node2, node3, node4, node5, link1, link2); // load model into engine and render engine.setModel(model); return ( { action('Serialized Graph')(JSON.stringify(model.serialize(), null, 2)); }} > Serialize Graph } > ); }; ================================================ FILE: diagrams-demo-gallery/demos/demo-zoom-to-fit/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoWorkspaceWidget, DemoButton } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * * Simple stress test of the system plus zoom to fit function * * @Author Dylan Vorster */ export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { generateNodes(model, i * 200, j * 100); } } //5) load model into engine engine.setModel(model); //6) render the diagram! return ( engine.zoomToFit()}>Zoom to fit}> ); }; function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) { //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100 + offsetX, 100 + offsetY); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(200 + offsetX, 100 + offsetY); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); } ================================================ FILE: diagrams-demo-gallery/demos/demo-zoom-to-fit-nodes/index.tsx ================================================ import createEngine, { DiagramModel, DefaultNodeModel } from '@projectstorm/react-diagrams'; import * as React from 'react'; import { DemoWorkspaceWidget, DemoButton } from '../helpers/DemoWorkspaceWidget'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; import { DemoCanvasWidget } from '../helpers/DemoCanvasWidget'; /** * * Simple stress test of the system plus zoom to fit function * * @Author Dylan Vorster */ export default () => { //1) setup the diagram engine var engine = createEngine(); //2) setup the diagram model var model = new DiagramModel(); for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { generateNodes(model, i * 200, j * 100); } } //5) load model into engine engine.setModel(model); //6) render the diagram! return ( engine.zoomToFitSelectedNodes(50)}>Zoom to fit} > ); }; function generateNodes(model: DiagramModel, offsetX: number, offsetY: number) { //3-A) create a default node var node1 = new DefaultNodeModel('Node 1', 'rgb(0,192,255)'); var port1 = node1.addOutPort('Out'); node1.setPosition(100 + offsetX, 100 + offsetY); //3-B) create another default node var node2 = new DefaultNodeModel('Node 2', 'rgb(192,255,0)'); var port2 = node2.addInPort('In'); node2.setPosition(200 + offsetX, 100 + offsetY); //3-C) link the 2 nodes together var link1 = port1.link(port2); //4) add the models to the root graph model.addAll(node1, node2, link1); } ================================================ FILE: diagrams-demo-gallery/demos/helpers/DemoCanvasWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; import { css, Global } from '@emotion/react'; export interface DemoCanvasWidgetProps { color?: string; background?: string; } namespace S { export const Container = styled.div<{ color: string; background: string }>` height: 100%; background-color: ${(p) => p.background}; background-size: 50px 50px; display: flex; > * { height: 100%; min-height: 100%; width: 100%; } background-image: linear-gradient( 0deg, transparent 24%, ${(p) => p.color} 25%, ${(p) => p.color} 26%, transparent 27%, transparent 74%, ${(p) => p.color} 75%, ${(p) => p.color} 76%, transparent 77%, transparent ), linear-gradient( 90deg, transparent 24%, ${(p) => p.color} 25%, ${(p) => p.color} 26%, transparent 27%, transparent 74%, ${(p) => p.color} 75%, ${(p) => p.color} 76%, transparent 77%, transparent ); `; export const Expand = css` html, body, #root { height: 100%; } `; } export class DemoCanvasWidget extends React.Component> { render() { return ( <> {this.props.children} ); } } ================================================ FILE: diagrams-demo-gallery/demos/helpers/DemoWorkspaceWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; export interface DemoWorkspaceWidgetProps { buttons?: any; } namespace S { export const Toolbar = styled.div` padding: 5px; display: flex; flex-shrink: 0; `; export const Content = styled.div` flex-grow: 1; height: 100%; `; export const Container = styled.div` background: black; display: flex; flex-direction: column; height: 100%; border-radius: 5px; overflow: hidden; `; } export const DemoButton = styled.button` background: rgb(60, 60, 60); font-size: 14px; padding: 5px 10px; border: none; color: white; outline: none; cursor: pointer; margin: 2px; border-radius: 3px; &:hover { background: rgb(0, 192, 255); } `; export class DemoWorkspaceWidget extends React.Component> { render() { return ( {this.props.buttons} {this.props.children} ); } } ================================================ FILE: diagrams-demo-gallery/demos/helpers/Helper.tsx ================================================ import * as React from 'react'; export class Helper { /** * Logs the mouse position in the console, but overlays a div that consumes all events * since the actual story book stories are rendered as an iFrame. */ static logMousePosition() { let element = window.parent.document.createElement('mouse-position'); element.style.position = 'absolute'; element.style.top = '0px'; element.style.left = '0px'; element.style.bottom = '0px'; element.style.right = '0px'; element.style.zIndex = '10'; window.parent.document.body.appendChild(element); window.parent.window.addEventListener('mousemove', (event) => { console.clear(); console.log(event.clientX, event.clientY); }); } } ================================================ FILE: diagrams-demo-gallery/demos/helpers/index.css ================================================ html, body, #storybook-root { height: 100%; padding: 0; margin: 0; } ================================================ FILE: diagrams-demo-gallery/package.json ================================================ { "name": "@projectstorm/react-diagrams-gallery", "version": "7.2.1", "author": "dylanvorster", "license": "MIT", "private": true, "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "start": "pnpm storybook dev", "storybook:build": "pnpm storybook build -c .storybook -o .out" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "dependencies": { "@projectstorm/react-canvas-core": "workspace:*", "@projectstorm/react-diagrams": "workspace:*", "@projectstorm/react-diagrams-core": "workspace:*", "@projectstorm/react-diagrams-defaults": "workspace:*", "gsap": "^3.12.2", "json-beautify": "^1.1.1", "lodash": "^4.17.21", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.27.0", "@storybook/addon-actions": "^8.6.9", "@storybook/addon-webpack5-compiler-babel": "^3.0.5", "@storybook/manager-api": "^8.6.10", "@storybook/preview-api": "^8.6.10", "@storybook/react": "^8.6.9", "@storybook/react-webpack5": "^8.6.9", "@storybook/storybook-deployer": "^2.8.16", "@storybook/theming": "^8.6.9", "@types/lodash": "^4.14.200", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "storybook": "^8.6.9" } } ================================================ FILE: diagrams-demo-gallery/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "esModuleInterop": true, "declaration": true, "composite": true, "incremental": true, "strictNullChecks": false, "sourceMap": true, "skipLibCheck": true, "jsx": "react", "target": "ES6", "module": "commonjs", "strict": false, "lib": [ "DOM", "ES6" ] }, "include": [ "demos" ] } ================================================ FILE: diagrams-demo-project/.babelrc ================================================ { "presets": [ ["@babel/preset-env",{ "targets": { "node": true } }], "@babel/preset-react" ] } ================================================ FILE: diagrams-demo-project/CHANGELOG.md ================================================ # @projectstorm/react-diagrams-demo ## 7.0.4 ### Patch Changes - @projectstorm/react-diagrams@7.0.4 ## 7.0.3 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - @projectstorm/react-diagrams@7.0.3 ## 7.0.2 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/react-diagrams@7.0.2 ## 7.0.1 ### Patch Changes - @projectstorm/react-diagrams@7.0.1 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-diagrams@7.0.0 ================================================ FILE: diagrams-demo-project/README.md ================================================ # Project STORM > React diagrams > Demo Project ![](./screenshot.png) In this repo you will find a simple webpack-dev-server project that shows how to get started with the library. It contains an example of how to implement a custom node in both Vanilla ES6 as-well as typescript (the recommended way). Simply run `yarn start` which will also open your browser. ================================================ FILE: diagrams-demo-project/index.html ================================================ Project STORM | React Diagrams demo
================================================ FILE: diagrams-demo-project/package.json ================================================ { "name": "@projectstorm/react-diagrams-demo", "version": "7.0.4", "author": "dylanvorster", "license": "MIT", "private": true, "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "start": "./node_modules/.bin/webpack serve --open" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@projectstorm/react-diagrams": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@babel/core": "^7.26.10", "@babel/preset-react": "^7.26.3", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", "html-webpack-plugin": "^5.5.3", "source-map-loader": "^4.0.1", "style-loader": "^3.3.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } } ================================================ FILE: diagrams-demo-project/src/BodyWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine, CanvasWidget } from '@projectstorm/react-diagrams'; export interface BodyWidgetProps { engine: DiagramEngine; } export class BodyWidget extends React.Component { render() { return ; } } ================================================ FILE: diagrams-demo-project/src/custom-node-js/JSCustomNodeFactory.jsx ================================================ import * as React from 'react'; import { JSCustomNodeModel } from './JSCustomNodeModel'; import { JSCustomNodeWidget } from './JSCustomNodeWidget'; import { AbstractReactFactory } from '@projectstorm/react-diagrams'; export class JSCustomNodeFactory extends AbstractReactFactory { constructor() { super('js-custom-node'); } generateModel(event) { return new JSCustomNodeModel(); } generateReactWidget(event) { return ; } } ================================================ FILE: diagrams-demo-project/src/custom-node-js/JSCustomNodeModel.js ================================================ import { DefaultPortModel, NodeModel } from '@projectstorm/react-diagrams'; /** * Example of a custom model using pure javascript */ export class JSCustomNodeModel extends NodeModel { constructor(options = {}) { super({ ...options, type: 'js-custom-node' }); this.color = options.color || { options: 'red' }; // setup an in and out port this.addPort( new DefaultPortModel({ in: true, name: 'in' }) ); this.addPort( new DefaultPortModel({ in: false, name: 'out' }) ); } serialize() { return { ...super.serialize(), color: this.color }; } deserialize(ob, engine) { super.deserialize(ob, engine); this.color = ob.color; } } ================================================ FILE: diagrams-demo-project/src/custom-node-js/JSCustomNodeWidget.jsx ================================================ import * as React from 'react'; import { PortWidget } from '@projectstorm/react-diagrams'; export class JSCustomNodeWidget extends React.Component { render() { return (
); } } ================================================ FILE: diagrams-demo-project/src/custom-node-ts/TSCustomNodeFactory.tsx ================================================ import * as React from 'react'; import { TSCustomNodeModel } from './TSCustomNodeModel'; import { TSCustomNodeWidget } from './TSCustomNodeWidget'; import { AbstractReactFactory } from '@projectstorm/react-diagrams'; import { DiagramEngine } from '@projectstorm/react-diagrams'; import { JSX } from 'react'; export class TSCustomNodeFactory extends AbstractReactFactory { constructor() { super('ts-custom-node'); } generateModel(initialConfig) { return new TSCustomNodeModel(); } generateReactWidget(event): JSX.Element { return ; } } ================================================ FILE: diagrams-demo-project/src/custom-node-ts/TSCustomNodeModel.ts ================================================ import { BaseModelOptions, DefaultPortModel, NodeModel } from '@projectstorm/react-diagrams'; export interface TSCustomNodeModelOptions extends BaseModelOptions { color?: string; } export class TSCustomNodeModel extends NodeModel { color: string; constructor(options: TSCustomNodeModelOptions = {}) { super({ ...options, type: 'ts-custom-node' }); this.color = options.color || 'red'; // setup an in and out port this.addPort( new DefaultPortModel({ in: true, name: 'in' }) ); this.addPort( new DefaultPortModel({ in: false, name: 'out' }) ); } serialize() { return { ...super.serialize(), color: this.color }; } deserialize(event): void { super.deserialize(event); this.color = event.data.color; } } ================================================ FILE: diagrams-demo-project/src/custom-node-ts/TSCustomNodeWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine, PortWidget } from '@projectstorm/react-diagrams'; import { TSCustomNodeModel } from './TSCustomNodeModel'; export interface TSCustomNodeWidgetProps { node: TSCustomNodeModel; engine: DiagramEngine; } export interface TSCustomNodeWidgetState {} export class TSCustomNodeWidget extends React.Component { constructor(props: TSCustomNodeWidgetProps) { super(props); this.state = {}; } render() { return (
); } } ================================================ FILE: diagrams-demo-project/src/main.css ================================================ *{ margin: 0; padding: 0; } html, body, #application{ height: 100%; overflow: hidden; } .diagram-container{ background: #333333; width: 100%; height: 100%; } .custom-node{ border: solid 2px gray; border-radius: 5px; width: 50px; height: 50px; display: flex; align-items: flex-start; justify-content: space-between; position: relative; } .custom-node-color{ position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; transform: translate(-50%, -50%); border-radius: 10px; } .circle-port{ width: 12px; height: 12px; margin: 2px; border-radius: 4px; background: darkgray; cursor: pointer; } .circle-port:hover{ background: mediumpurple; } ================================================ FILE: diagrams-demo-project/src/main.tsx ================================================ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import './main.css'; import createEngine, { DefaultLinkModel, DiagramModel } from '@projectstorm/react-diagrams'; import { JSCustomNodeFactory } from './custom-node-js/JSCustomNodeFactory'; import { TSCustomNodeFactory } from './custom-node-ts/TSCustomNodeFactory'; import { JSCustomNodeModel } from './custom-node-js/JSCustomNodeModel'; import { TSCustomNodeModel } from './custom-node-ts/TSCustomNodeModel'; import { BodyWidget } from './BodyWidget'; // create an instance of the engine const engine = createEngine(); // register the two engines engine.getNodeFactories().registerFactory(new JSCustomNodeFactory() as any); engine.getNodeFactories().registerFactory(new TSCustomNodeFactory()); // create a diagram model const model = new DiagramModel(); //#################################################### // now create two nodes of each type, and connect them const node1 = new JSCustomNodeModel({ color: 'rgb(192,255,0)' }); node1.setPosition(50, 50); const node2 = new TSCustomNodeModel({ color: 'rgb(0,192,255)' }); node2.setPosition(200, 50); const link1 = new DefaultLinkModel(); link1.setSourcePort(node1.getPort('out')); link1.setTargetPort(node2.getPort('in')); model.addAll(node1, node2, link1); //#################################################### // install the model into the engine engine.setModel(model); document.addEventListener('DOMContentLoaded', () => { const root = createRoot(document.querySelector('#application')); root.render(); }); ================================================ FILE: diagrams-demo-project/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "declaration": false, "jsx": "react", "allowJs": true, "target": "es6", "module": "CommonJS" }, "include": [ "./src" ] } ================================================ FILE: diagrams-demo-project/webpack.config.js ================================================ const path = require('path'); const production = process.env.NODE_ENV === 'production'; const TerserPlugin = require('terser-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: production ? 'production' : 'development', devtool: 'inline-source-map', entry: './src/main.tsx', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, optimization: { minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { ecma: 6 } }) ] }, plugins: [ new HtmlWebpackPlugin({ template: 'index.html' }) ], module: { rules: [ { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.jsx?$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.tsx?$/, loader: 'ts-loader' } ] }, devServer: { client: { overlay: true }, hot: false, compress: true } }; ================================================ FILE: docs/README.md ================================================ # Table of contents * [Introduction](README.md) * [Getting Started](getting-started/README.md) * [Using the library](getting-started/using-the-library.md) * [Customizing](customizing/README.md) * [Extending DefaultLinkModel](customizing/extending-default-links.md) * [Custom Nodes](customizing/nodes.md) * [Custom Ports](customizing/ports.md) * [About the project](about-the-project/README.md) * [Testing](about-the-project/testing.md) * [Architecture Questions](about-the-project/architecture-questions.md) ================================================ FILE: docs/about-the-project/architecture-questions.md ================================================ # Architecture Questions Here I will try to answer any questions relating to the design of the system ## What was the inspiration for this library? Joint JS \(a fantastic library\) + my need for rich HTML nodes + LabView + Blender Composite sub system ## Why render the nodes as HTML Elements and not SVG's? My original requirement for this library stemmed from the requirement of wanting HTML nodes that would allow me to embed rich controls such as input fields, dropdowns and have the system treat such nodes as first class citizens. I originally tried to make this work in JointJS, but ran into a number of problems of which this was a relatively big one. JointJS does allow you to do this, but at the time of writing this library originally, I was having a lot of trouble to make it work exactly like I needed it, and therefore decided from the very beginning that I would attempt this with an HTML first mindset. ## Why Typescript? Firstly, because it can transpile into any level of ECMAScript. This means that I don't need to break our the refactor tractor every time ECMAScript decides it wants to add features which it should have done years ago. I also ported it to Typescript to accommodate the heavy architectural changes I was starting to make. Since porting the library to typescript, and seeing the project explode in size and complexity, I consider this the best decision made with regard to this library so far. Porting to typescript also afforded us a set of powerful features such as generics and static analysis that all the project contributors have made exclusive use of. Typescript is <3 typescript is life. ## Why not Flow instead of Typescript? At the time when I first started evaluating languages that could transpile to ECMAScript, I was not so sold on the supporting environment surrounding flow, and found that there was better tooling to support typescript, they are ultimately trying to do the same thing though, and I guess in the end, typescript just made more sense. ## Why React ? React is really efficient at rendering and managing HTML in a declarative manner. React has also become one of the bigger industry standards and has a rich ecosystem that plays really well with typescript. Apart from these notable points, I am really fond of React and wanted a diagramming library that takes full advantage of it, and makes it easy for engineers to use its power as well, when extending this library. ## Why cant the Default models and widgets do this or that ? They are intended to illustrate **how** to use this library and act as a good starting point to extend and show the capability. Ultimately I designed this library to be completely pluggable in a way that you can use it as a library and not a framework. If the default widgets are not good enough, then a good place to start is with creating your own models/factories/widgets. ## Model vs Widget For those that are new to [Scene Graphs](https://en.wikipedia.org/wiki/Scene_graph) or are not familiar with concepts such as [MVC](https://en.wikipedia.org/wiki/Model–view–controller), this library represents your entire graph as a model. The model is a traversable graph that represents the nodes and links between them in a virtual manner. Your program \(aka the business logic/layer\) can mutate this model imperatively or store snapshots decoratively of the complete model \(via serialization\) and then the engine and react widgets will take care of the rendering. For this reason every model in the library is represented by a widget, and the factories glue it all together. ## How do I make my own elements? Take a look at the **demos** directory, with specific attention to the **DefaultNodeWidget** That being said, the demos directory is an _example_ of how you can create your own elements. A number of people want to use the defaults as is, which is cool, but is recommended to create your own models/factories/widgets. ## How do I use the library? Take a look at the demo folders, they have simple and complex examples of the complete usage. A good example of a real-world example is Demo 5 ================================================ FILE: docs/about-the-project/testing.md ================================================ # Testing ## End to end testing To test the functionality of the library, we make use of e2e tests \(end to end tests\). In this library, we spin up a headless chrome using puppeteer and interactively and programmatically tell the mouse pointer to click and drag on various elements while making assertions along the way. We use Jest for the assertions and the interactivity is handled by puppeteer. Due to the laborious nature of writing e2e tests, there is a helper method that is provided in each test that makes interacting with the diagrams a lot easier. Using this helper, you can easily tell the mouse to drag links between nodes, select them and also easily assert information about them. The important thing here, is that this helper does not touch the model in any way, but is purely a helper for writing the tests themselves. Please make use of this helper when writing tests, as it ensure that the tests are defensive in nature, and also reduces the overhead of physically writing them. ================================================ FILE: docs/customizing/README.md ================================================ # Customizing Almost all components in react-diagrams are customizable. While some customization is better documented than others, the best way to learn about customization is through the examples in the codebase and by looking at the type annotations that come with the library. Most UI customization can be done through extending existing base classes. While node, port, and link have different data models, they share the same customization pattern: - they need a **model factory** extended off `AbstractModelFactory`, and that factory needs to be registered with the engine under a different model type - optionally, if you data model is different from the default, you can extend existing base classes such as `NodeModel`, `PortModel`, `DefaultLinkModel`, etc. - they need to have a **custom component** which renders using its default or customized data model. Some component such as the port can also be extended with composition such as port if you want to simply change the appearance. ## Working with custom links This is the easiest way to get started: [Extending the default Link](./extending-default-links.md) ## Working with custom nodes [Working with Nodes](./nodes.md) [Working with Ports](./ports.md) ================================================ FILE: docs/customizing/extending-default-links.md ================================================ # Custom Links ## Extending the DefaultLinkModel Much like extending nodes, custom links can also be created. In the below example, we have created a link that renders a circle animating from the source port to the target port. ![](./images/custom-link.png) In this specific example, we extended the `DefaultLinkModel` because we wanted to retain a lot of the functionality that it provides in the base class: ```typescript export class AdvancedLinkModel extends DefaultLinkModel { constructor() { super({ type: 'advanced', // <-- here we give it a new type width: 10 // we specifically want this to also be width 10 }); } } ``` Now we need to create a new link factory to tell the system how our new link model fits into the core system. We specifically are going to extend the `DefaultLinkFactory` because we still want to render a `DefaultLinkWidget`. The only difference is that we want each __path segment__ to be a red line with an animating circle. Fortunately, the `DefaultLinkWidget` already uses the `generateLinkSegment()` method defined in the `DefaultLinkFactory` to accomplish this. The only thing we need to do, is provide a different type of segment: ```typescript export class AdvancedLinkFactory extends DefaultLinkFactory { constructor() { super('advanced'); // <-- this matches with the link model above } generateModel(): AdvancedLinkModel { return new AdvancedLinkModel(); // <-- this is how we get new instances } /** * @override the DefaultLinkWidget makes use of this, and it normally renders that * familiar gray line, so in this case we simply make it return a new advanced segment. */ generateLinkSegment(model: AdvancedLinkModel, selected: boolean, path: string) { return ( ); } } ``` The actual code for the `AdvancedLinkSegment` [can be found here](https://github.com/projectstorm/react-diagrams/tree/master/diagrams-demo-gallery/demos/demo-custom-link1) (it is in the `demo-custom-link1` folder in the demo gallery). This is the easiest and most simple way to get started with custom links. ================================================ FILE: docs/customizing/nodes.md ================================================ # Nodes A node contains the node content itself and its ports. Check [NodeModel source code](https://github.com/projectstorm/react-diagrams/blob/master/packages/react-diagrams-core/src/entities/node/NodeModel.ts#L24), if you want to see what class methods can be extended. ## Extending the NodeModel If you want to create a custom node that looks entirely different, then you need to create a component that renders using its default or customized data mode. In the example below, it uses a customized data model `DiamondNodeModel` to render `DiamondNodeWidget`, and both of them are being created in the model factory `DiamondNodeFactory`. ![](./images/diamond-node.png) Because our Diamond node always has four ports, we add four default port models into the `DiamondNodeModel`. Depending on the type of node you are creating, this is basically where you store your vertex data in the graph theory sense. ```typescript // DiamondNodeModel.ts import { NodeModel, NodeModelGenerics, PortModelAlignment } from '@projectstorm/react-diagrams'; import { DiamondPortModel } from './DiamondPortModel'; export interface DiamondNodeModelGenerics { PORT: DiamondPortModel; } // this can be further extended for more complicated node types export class DiamondNodeModel extends NodeModel { constructor() { super({ type: 'diamond' }); this.addPort(new DiamondPortModel(PortModelAlignment.TOP)); this.addPort(new DiamondPortModel(PortModelAlignment.LEFT)); this.addPort(new DiamondPortModel(PortModelAlignment.BOTTOM)); this.addPort(new DiamondPortModel(PortModelAlignment.RIGHT)); } } ``` This is where we create our customized component. This component can be any customized react component as long as they respect the node and engine props. Ports also need to be rendered inside the node component. ```typescript // DiamondNodeWidget.tsx import * as React from 'react'; import { DiamondNodeModel } from './DiamondNodeModel'; import { DiagramEngine, PortModelAlignment, PortWidget } from '@projectstorm/react-diagrams'; import styled from '@emotion/styled'; export interface DiamondNodeWidgetProps { // node and engine props are required node: DiamondNodeModel; engine: DiagramEngine; size?: number; } namespace S { export const Port = styled.div` width: 16px; height: 16px; z-index: 10; background: rgba(0, 0, 0, 0.5); border-radius: 8px; cursor: pointer; &:hover { background: rgba(0, 0, 0, 1); } `; } // this can be any customized react component as long as they respect // the node and engine props export class DiamondNodeWidget extends React.Component { render() { return (
` }} />
); } } ``` Now we need to create a new node factory to tell the system how our new node model fits into the core system. We specifically are going to extend the `DefaultLinkFactory` because we want to render a `DiamondNodeWidget` with data model being `DiamondNodeModel`. To accomplish that, we simply extend `generateReactWidget(event)` to return a `DiamondNodeWidget` and extend `generateModel` to return a `DiamondNodeModel` instance. ```typescript // DiamondNodeFactory.tsx import { DiamondNodeWidget } from './DiamondNodeWidget'; import { DiamondNodeModel } from './DiamondNodeModel'; import * as React from 'react'; import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; export class DiamondNodeFactory extends AbstractReactFactory { constructor() { super('diamond'); } generateReactWidget(event): JSX.Element { // event.model is basically what's returned from generateModel() return ; } generateModel(event) { return new DiamondNodeModel(); } } ``` The actual code for the `DiamondNode` [can be found here](https://github.com/projectstorm/react-diagrams/tree/master/diagrams-demo-gallery/demos/demo-custom-node1) (it is in the `demo-custom-node1` folder in the demo gallery). This is the easiest and most simple way to get started with custom nodes. ================================================ FILE: docs/customizing/ports.md ================================================ # Ports Ports allow links to connect to your nodes. Each port that is rendered in a node must also have a corresponding PortModel in the corresponding NodeModel (as is the case with essentially all of the models and widgets in this library). ## Custom port widgets If you want to create a custom port that looks entirely different (much like in the image below), then you simply need to create your own widget and wrap it in a `PortWidget`: ```jsx
``` Obviously, you can create the React widgets in any way you like. Whether you use __Emotion__, __BEM__ or plain old __CSS__, the only important thing is that your custom port is wrapped inside a `PortWidget` ## Specifying alignment When links enter ports, depending on the alignment specified, they can help the links render differently. Take the following example: ![](./images/diamond-node.png) In the above example, the 4 ports on the diamond node model are setup with different alignment: ```typescript this.addPort(new DiamondPortModel(PortModelAlignment.TOP)); this.addPort(new DiamondPortModel(PortModelAlignment.LEFT)); this.addPort(new DiamondPortModel(PortModelAlignment.BOTTOM)); this.addPort(new DiamondPortModel(PortModelAlignment.RIGHT)); ``` Each of the custom `DiamondPortModel` models forwards this through to the base `PortModel` class: ```typescript export class DiamondPortModel extends PortModel { ... constructor(alignment: PortModelAlignment) { super({ type: 'diamond', name: alignment, alignment: alignment // <-- here }); } ... } ``` ## Specifying if a link can be connected A port is directly responsible for specifying if a link is allowed to connect to it. When you drag an un-connected link end-point to a target port, the target port lets the link know if it is allowed to connect. ```typescript class PortModel{ ... canLinkToPort(port: PortModel): boolean; } ``` In the above definition, the port argument is the source port that the incoming link is connected to. By default, the method returns true, but you can extend this and overide this method to do more advanced checks. The `DefaultPortModel` provided in the defaults package, makes use of this principle to only allow `Out` ports to connect to `In` ports: ```typescript class DefaultPortModel extends PortModel{ ... canLinkToPort(port: PortModel): boolean { if (port instanceof DefaultPortModel) { return this.options.in !== port.getOptions().in; } return true; } } ``` ## Specifying what type of link is generated from a port When a user drags on a port to generate a link, the port is also responsible for specifying what link is created. This happens through the `createLinkModel()` method: ```typescript class DefaultPortModel extends PortModel{ ... createLinkModel(): LinkModel{ return new DefaultLinkModel(); // <-- here we generate a DefaultLinkModel } } ``` ================================================ FILE: docs/getting-started/README.md ================================================ # Getting Started ## Get the package The first thing you need to do, is grab the distribution files on NPM. **Via yarn:** ```text yarn add @projectstorm/react-diagrams ``` **Via npm:** ```text npm install @projectstorm/react-diagrams ``` **Via pnpm:** ```text pnpm add @projectstorm/react-diagrams ``` When you run this in your project directory, this will install the library into `./node_modules/@projectstorm/react-diagrams`. You will then find a **dist** folder that contains all the minified and production ready code. ================================================ FILE: docs/getting-started/using-the-library.md ================================================ # Using the library ## Using Typescript If you are using typescript, then you are in luck! The library is built in typescript, and includes advanced types for everything you need right out of the box. Lets start by including the things we are going to need: ```typescript import createEngine, { DefaultLinkModel, DefaultNodeModel, DiagramModel } from '@projectstorm/react-diagrams'; import { CanvasWidget } from '@projectstorm/react-canvas-core'; ``` Now we call `createEngine` which will bootstrap a **DiagramEngine** for us that contains all the defaults setup. ```typescript // create an instance of the engine with all the defaults const engine = createEngine(); ``` Next, we create two nodes: ```typescript // node 1 const node1 = new DefaultNodeModel({ name: 'Node 1', color: 'rgb(0,192,255)', }); node1.setPosition(100, 100); let port1 = node1.addOutPort('Out'); // node 2 const node2 = new DefaultNodeModel({ name: 'Node 1', color: 'rgb(0,192,255)', }); node2.setPosition(100, 100); let port2 = node2.addOutPort('Out'); ``` Now we link the two ports of both of the nodes: ```typescript // link them and add a label to the link const link = port1.link(port2); link.addLabel('Hello World!'); ``` Great! Now we have setup a simple diagram. All thats left to do, is create a **DiagramModel** to contain everything, add all the elements to it, and then add it to the engine. ```typescript const model = new DiagramModel(); model.addAll(node1, node2, link); engine.setModel(model); ``` And then we render with **React**! ```jsx ``` ================================================ FILE: package.json ================================================ { "name": "@projectstorm/react-diagrams", "author": "dylanvorster", "private": true, "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "scripts": { "ncu": "ncu -u && pnpm recursive exec -- ncu -u", "format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"", "clean": "rm -rf packages/*/dist", "test": "pnpm run -r test", "build": "tsc --build && pnpm run -r build", "build:prod": "NODE_ENV=production pnpm build", "release": "pnpm build:prod && pnpm changeset publish", "release:storybook": "tsc --build && cd diagrams-demo-gallery && pnpm storybook:build && ./node_modules/.bin/storybook-to-ghpages --existing-output-dir .out" }, "devDependencies": { "@changesets/cli": "^2.26.2", "@types/jest": "^29.5.5", "@types/node": "^20.6.3", "jest": "^29.7.0", "jest-cli": "^29.7.0", "prettier": "^3.0.3", "rimraf": "^5.0.1", "source-map-loader": "^4.0.1", "terser-webpack-plugin": "^5.3.9", "ts-jest": "^29.1.1", "ts-loader": "^9.4.4", "typescript": "^5.2.2", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", "webpack-node-externals": "^3.0.0" }, "pnpm": { "overrides": { "react": "^19.0.0" } } } ================================================ FILE: packages/geometry/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/geometry/CHANGELOG.md ================================================ # @projectstorm/geometry ## 7.0.3 ### Patch Changes - 80285fe: refactor: update lodash imports to use individual functions ## 7.0.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade ## 7.0.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ================================================ FILE: packages/geometry/package.json ================================================ { "name": "@projectstorm/geometry", "version": "7.0.3", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "lodash": "^4.17.21" }, "devDependencies": { "@types/lodash": "^4.14.200" } } ================================================ FILE: packages/geometry/src/BezierCurve.ts ================================================ import { Point } from './Point'; import { Polygon } from './Polygon'; export enum BezierCurvepPoints { SOURCE = 0, SOURCE_CONTROL = 1, TARGET_CONTROL = 2, TARGET = 3 } export class BezierCurve extends Polygon { constructor() { super([new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]); } getSVGCurve(): string { return `M${this.getSource().toSVG()} C${this.getSourceControl().toSVG()}, ${this.getTargetControl().toSVG()}, ${this.getTarget().toSVG()}`; } setPoints(points: Point[]) { if (points.length !== 4) { throw new Error('BezierCurve must have extactly 4 points'); } super.setPoints(points); } getSource(): Point { return this.points[BezierCurvepPoints.SOURCE]; } getSourceControl(): Point { return this.points[BezierCurvepPoints.SOURCE_CONTROL]; } getTargetControl(): Point { return this.points[BezierCurvepPoints.TARGET_CONTROL]; } getTarget(): Point { return this.points[BezierCurvepPoints.TARGET]; } setSource(point: Point) { this.points[BezierCurvepPoints.SOURCE] = point; } setSourceControl(point: Point) { this.points[BezierCurvepPoints.SOURCE_CONTROL] = point; } setTargetControl(point: Point) { this.points[BezierCurvepPoints.TARGET_CONTROL] = point; } setTarget(point: Point) { this.points[BezierCurvepPoints.TARGET] = point; } } ================================================ FILE: packages/geometry/src/Bounds.ts ================================================ import { Point } from './Point'; export enum BoundsCorner { TOP_LEFT = 'TL', TOP_RIGHT = 'TR', BOTTOM_RIGHT = 'BR', BOTTOM_LEFT = 'BL' } export type Bounds = { [k in BoundsCorner]: Point }; export const boundsFromPositionAndSize = (x: number, y: number, width: number, height: number): Bounds => { return { [BoundsCorner.TOP_LEFT]: new Point(x, y), [BoundsCorner.TOP_RIGHT]: new Point(x + width, y), [BoundsCorner.BOTTOM_RIGHT]: new Point(x + width, y + height), [BoundsCorner.BOTTOM_LEFT]: new Point(x, y + height) }; }; export const createEmptyBounds = () => { return { [BoundsCorner.TOP_LEFT]: new Point(), [BoundsCorner.TOP_RIGHT]: new Point(), [BoundsCorner.BOTTOM_RIGHT]: new Point(), [BoundsCorner.BOTTOM_LEFT]: new Point() }; }; ================================================ FILE: packages/geometry/src/Matrix.ts ================================================ import { Point } from './Point'; export class Matrix { matrix: number[][]; constructor(matrix: number[][]) { this.matrix = matrix; } mmul(matrix: Matrix): Matrix { this.matrix = this.matrix.map((row, i) => matrix.asArray()[0].map((_, j) => row.reduce((acc, _, n) => acc + this.matrix[i][n] * matrix.asArray()[n][j], 0)) ); return this; } asArray(): number[][] { return this.matrix; } get(rowIndex: number, columnIndex: number): number { return this.asArray()[rowIndex][columnIndex]; } public static multiply(...matrices: Matrix[]): Matrix { let m: Matrix = matrices[0]; for (let i = 1; i < matrices.length; i++) { m = m.mmul(matrices[i]); } return m; } public static scaleMatrix(x: number, y: number): Matrix { return new Matrix([ [x, 0, 0], [0, y, 0], [0, 0, 1] ]); } public static translateMatrix(x: number, y: number): Matrix { return new Matrix([ [1, 0, x], [0, 1, y], [0, 0, 1] ]); } public static rotateMatrix(deg: number): Matrix { return new Matrix([ [Math.cos(deg), -1 * Math.sin(deg), 0], [Math.sin(deg), Math.cos(deg), 0], [0, 0, 1] ]); } static createScaleMatrix(x, y, origin: Point): Matrix { return this.multiply( Matrix.translateMatrix(origin.x, origin.y), Matrix.scaleMatrix(x, y), Matrix.translateMatrix(-origin.x, -origin.y) ); } static createRotateMatrix(deg: number, origin: Point): Matrix { return this.multiply( Matrix.translateMatrix(origin.x, origin.y), Matrix.rotateMatrix(deg), Matrix.translateMatrix(-origin.x, -origin.y) ); } } ================================================ FILE: packages/geometry/src/Point.ts ================================================ import { Matrix } from './Matrix'; export class Point { x: number; y: number; constructor(x: number = 0, y: number = 0) { this.x = x; this.y = y; } translate(x: number, y: number) { this.x += x; this.y += y; } clone() { return new Point(this.x, this.y); } toSVG() { return this.x + ' ' + this.y; } asMatrix() { return new Matrix([[this.x], [this.y], [1]]); } transform(matrix: Matrix) { let final: Matrix = matrix.mmul(this.asMatrix()); this.x = final.get(0, 0); this.y = final.get(1, 0); } public static middlePoint(pointA: Point, pointB: Point): Point { return new Point((pointB.x + pointA.x) / 2, (pointB.y + pointA.y) / 2); } } ================================================ FILE: packages/geometry/src/Polygon.ts ================================================ import { Point } from './Point'; import _forEach from 'lodash/forEach'; import _map from 'lodash/map'; import { Matrix } from './Matrix'; import { boundingBoxFromPoints } from './toolkit'; import { Bounds, BoundsCorner } from './Bounds'; export class Polygon { protected points: Point[]; constructor(points: Point[] = []) { this.points = points; } serialize() { return _map(this.points, (point) => { return [point.x, point.y]; }); } deserialize(data: any) { this.points = _map(data, (point) => { return new Point(point[0], point[1]); }); } scale(x, y, origin: Point) { let matrix = Matrix.createScaleMatrix(x, y, origin); _forEach(this.points, (point) => { point.transform(matrix); }); } transform(matrix: Matrix) { _forEach(this.points, (point) => { point.transform(matrix); }); } setPoints(points: Point[]) { this.points = points; } getPoints(): Point[] { return this.points; } rotate(degrees: number) { this.transform(Matrix.createRotateMatrix(degrees / (180 / Math.PI), this.getOrigin())); } translate(offsetX: number, offsetY: number) { _forEach(this.points, (point) => { point.translate(offsetX, offsetY); }); } doClone(ob: this) { this.points = _map(ob.points, (point) => { return point.clone(); }); } clone(): this { let ob = Object.create(this); ob.doClone(this); return ob; } getOrigin(): Point { if (this.points.length === 0) { return null; } let dimensions = boundingBoxFromPoints(this.points); return Point.middlePoint(dimensions[BoundsCorner.TOP_LEFT], dimensions[BoundsCorner.BOTTOM_RIGHT]); } getBoundingBox(): Bounds { return boundingBoxFromPoints(this.points); } } ================================================ FILE: packages/geometry/src/Rectangle.ts ================================================ import { Point } from './Point'; import { Polygon } from './Polygon'; import { Bounds, BoundsCorner, boundsFromPositionAndSize, createEmptyBounds } from './Bounds'; export class Rectangle extends Polygon { static fromPositionAndSize(x: number, y: number, width: number, height: number) { return new Rectangle(boundsFromPositionAndSize(x, y, width, height)); } static fromPointAndSize(position: Point, width: number, height: number) { return new Rectangle(boundsFromPositionAndSize(position.x, position.y, width, height)); } constructor(points?: Bounds) { if (!points) { points = createEmptyBounds(); } super([ points[BoundsCorner.TOP_LEFT], points[BoundsCorner.TOP_RIGHT], points[BoundsCorner.BOTTOM_RIGHT], points[BoundsCorner.BOTTOM_LEFT] ]); } updateDimensions(x: number, y: number, width: number, height: number) { const points = boundsFromPositionAndSize(x, y, width, height); this.setPoints([ points[BoundsCorner.TOP_LEFT], points[BoundsCorner.TOP_RIGHT], points[BoundsCorner.BOTTOM_RIGHT], points[BoundsCorner.BOTTOM_LEFT] ]); } setPoints(points: Point[]) { if (points.length !== 4) { throw 'Rectangles must always have 4 points'; } super.setPoints(points); } containsPoint(point: Point) { const tl = this.getTopLeft(); const br = this.getBottomRight(); return point.x >= tl.x && point.x <= br.x && point.y >= tl.y && point.y <= br.y; } getWidth(): number { return Math.sqrt( Math.pow(this.getTopLeft().x - this.getTopRight().x, 2) + Math.pow(this.getTopLeft().y - this.getTopRight().y, 2) ); } getHeight(): number { return Math.sqrt( Math.pow(this.getBottomLeft().x - this.getTopLeft().x, 2) + Math.pow(this.getBottomLeft().y - this.getTopLeft().y, 2) ); } getTopMiddle(): Point { return Point.middlePoint(this.getTopLeft(), this.getTopRight()); } getBottomMiddle(): Point { return Point.middlePoint(this.getBottomLeft(), this.getBottomRight()); } getLeftMiddle(): Point { return Point.middlePoint(this.getBottomLeft(), this.getTopLeft()); } getRightMiddle(): Point { return Point.middlePoint(this.getBottomRight(), this.getTopRight()); } getTopLeft(): Point { return this.points[0]; } getTopRight(): Point { return this.points[1]; } getBottomRight(): Point { return this.points[2]; } getBottomLeft(): Point { return this.points[3]; } } ================================================ FILE: packages/geometry/src/index.ts ================================================ export * from './Point'; export * from './Matrix'; export * from './Polygon'; export * from './Rectangle'; export * from './BezierCurve'; export * from './toolkit'; export * from './Bounds'; ================================================ FILE: packages/geometry/src/toolkit.ts ================================================ import { Point } from './Point'; import _flatMap from 'lodash/flatMap'; import { Polygon } from './Polygon'; import { Bounds, BoundsCorner, createEmptyBounds } from './Bounds'; export const boundingBoxFromPoints = (points: Point[]): Bounds => { if (points.length === 0) { return createEmptyBounds(); } let minX = points[0].x; let maxX = points[0].x; let minY = points[0].y; let maxY = points[0].y; for (let i = 1; i < points.length; i++) { if (points[i].x < minX) { minX = points[i].x; } if (points[i].x > maxX) { maxX = points[i].x; } if (points[i].y < minY) { minY = points[i].y; } if (points[i].y > maxY) { maxY = points[i].y; } } return { [BoundsCorner.TOP_LEFT]: new Point(minX, minY), [BoundsCorner.TOP_RIGHT]: new Point(maxX, minY), [BoundsCorner.BOTTOM_RIGHT]: new Point(maxX, maxY), [BoundsCorner.BOTTOM_LEFT]: new Point(minX, maxY) }; }; export const boundingBoxFromPolygons = (polygons: Polygon[]): Bounds => { return boundingBoxFromPoints( _flatMap(polygons, (polygon) => { return polygon.getPoints(); }) ); }; ================================================ FILE: packages/geometry/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "outDir": "dist", "rootDir": "src", "sourceMap": true, "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "include": ["./src"] } ================================================ FILE: packages/geometry/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: '@projectstorm/react-diagrams-geometry' } }; ================================================ FILE: packages/react-canvas-core/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/react-canvas-core/CHANGELOG.md ================================================ # @projectstorm/react-canvas-core ## 7.0.3 ### Patch Changes - 09ed60f: Allow more derived State classes to provide a generic type - 80285fe: refactor: update lodash imports to use individual functions - Updated dependencies [80285fe] - @projectstorm/geometry@7.0.3 ## 7.0.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - Updated dependencies [66c687a] - @projectstorm/geometry@7.0.2 ## 7.0.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/geometry@7.0.1 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/geometry@7.0.0 ================================================ FILE: packages/react-canvas-core/package.json ================================================ { "name": "@projectstorm/react-canvas-core", "version": "7.0.3", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.11.0", "@projectstorm/geometry": "workspace:*", "lodash": "^4.17.21", "react": "^19.0.0" }, "devDependencies": { "@types/lodash": "^4.14.200", "@types/react": "^19.0.12" } } ================================================ FILE: packages/react-canvas-core/src/CanvasEngine.ts ================================================ import _debounce from 'lodash/debounce'; import { CanvasModel } from './entities/canvas/CanvasModel'; import { FactoryBank } from './core/FactoryBank'; import { AbstractReactFactory } from './core/AbstractReactFactory'; import { LayerModel } from './entities/layer/LayerModel'; import { BaseListener, BaseObserver } from './core/BaseObserver'; import { MouseEvent } from 'react'; import { BaseModel } from './core-models/BaseModel'; import { Point } from '@projectstorm/geometry'; import { ActionEventBus } from './core-actions/ActionEventBus'; import { PanAndZoomCanvasAction } from './actions/PanAndZoomCanvasAction'; import { ZoomCanvasAction } from './actions/ZoomCanvasAction'; import { DeleteItemsAction } from './actions/DeleteItemsAction'; import { StateMachine } from './core-state/StateMachine'; export interface CanvasEngineListener extends BaseListener { canvasReady?(): void; repaintCanvas?(): void; rendered?(): void; } /** * Defines the CanvasEngine options */ export interface CanvasEngineOptions { registerDefaultDeleteItemsAction?: boolean; registerDefaultPanAndZoomCanvasAction?: boolean; registerDefaultZoomCanvasAction?: boolean; /** * Defines the debounce wait time in milliseconds if > 0 */ repaintDebounceMs?: number; } export class CanvasEngine< L extends CanvasEngineListener = CanvasEngineListener, M extends CanvasModel = CanvasModel > extends BaseObserver { protected model: M; protected layerFactories: FactoryBank>; protected canvas: HTMLDivElement; protected eventBus: ActionEventBus; protected stateMachine: StateMachine; protected options: CanvasEngineOptions; constructor(options: CanvasEngineOptions = {}) { super(); this.model = null; this.eventBus = new ActionEventBus(this); this.stateMachine = new StateMachine(this); this.layerFactories = new FactoryBank(); this.registerFactoryBank(this.layerFactories); /** * Overrides the standard options with the possible given options */ this.options = { registerDefaultDeleteItemsAction: true, registerDefaultZoomCanvasAction: true, repaintDebounceMs: 0, ...options }; if (this.options.registerDefaultZoomCanvasAction === true) { this.eventBus.registerAction(new ZoomCanvasAction()); } else if (this.options.registerDefaultPanAndZoomCanvasAction === true) { this.eventBus.registerAction(new PanAndZoomCanvasAction()); } if (this.options.registerDefaultDeleteItemsAction === true) { this.eventBus.registerAction(new DeleteItemsAction()); } } getStateMachine() { return this.stateMachine; } getRelativeMousePoint(event: { clientX: number; clientY: number }): Point { const point = this.getRelativePoint(event.clientX, event.clientY); return new Point( (point.x - this.model.getOffsetX()) / (this.model.getZoomLevel() / 100.0), (point.y - this.model.getOffsetY()) / (this.model.getZoomLevel() / 100.0) ); } getRelativePoint(x, y): Point { const canvasRect = this.canvas.getBoundingClientRect(); return new Point(x - canvasRect.left, y - canvasRect.top); } registerFactoryBank(factory: FactoryBank) { factory.registerListener({ factoryAdded: (event) => { event.factory.setDiagramEngine(this); }, factoryRemoved: (event) => { event.factory.setDiagramEngine(null); } }); } getActionEventBus() { return this.eventBus; } getLayerFactories() { return this.layerFactories; } getFactoryForLayer>(layer: LayerModel | string) { if (typeof layer === 'string') { return this.layerFactories.getFactory(layer); } return this.layerFactories.getFactory(layer.getType()); } setModel(model: M) { this.model = model; if (this.canvas) { requestAnimationFrame(() => { this.repaintCanvas(); }); } } getModel(): M { return this.model; } repaintCanvas(promise: true): Promise; repaintCanvas(): void; repaintCanvas(promise?): Promise | void { const { repaintDebounceMs } = this.options; /** * The actual repaint function */ const repaint = () => { this.iterateListeners((listener) => { if (listener.repaintCanvas) { listener.repaintCanvas(); } }); }; // if the `repaintDebounceMs` option is > 0, then apply the debounce let repaintFn = repaint; if (repaintDebounceMs > 0) { repaintFn = _debounce(repaint, repaintDebounceMs); } if (promise) { return new Promise((resolve) => { const l = this.registerListener({ rendered: () => { resolve(); l.deregister(); } } as L); repaintFn(); }); } repaintFn(); } setCanvas(canvas?: HTMLDivElement) { if (this.canvas !== canvas) { this.canvas = canvas; if (canvas) { this.fireEvent({}, 'canvasReady'); } } } getCanvas() { return this.canvas; } getMouseElement(event: MouseEvent): BaseModel { return null; } zoomToFit() { const xFactor = this.canvas.clientWidth / this.canvas.scrollWidth; const yFactor = this.canvas.clientHeight / this.canvas.scrollHeight; const zoomFactor = xFactor < yFactor ? xFactor : yFactor; this.model.setZoomLevel(this.model.getZoomLevel() * zoomFactor); this.model.setOffset(0, 0); this.repaintCanvas(); } } ================================================ FILE: packages/react-canvas-core/src/Toolkit.ts ================================================ export class Toolkit { static TESTING: boolean = false; static TESTING_UID = 0; /** * Generats a unique ID (thanks Stack overflow :3) * @returns {String} */ public static UID(): string { if (Toolkit.TESTING) { Toolkit.TESTING_UID++; return `${Toolkit.TESTING_UID}`; } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } public static closest(element: Element, selector: string) { if (!Element.prototype.closest) { Element.prototype.closest = function (s) { var el = this; do { if (Element.prototype.matches.call(el, s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } return element.closest(selector); } } ================================================ FILE: packages/react-canvas-core/src/actions/DeleteItemsAction.ts ================================================ import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { KeyboardEvent } from 'react'; import _forEach from 'lodash/forEach'; import _isEqual from 'lodash/isEqual'; export interface DeleteItemsActionOptions { keyCodes?: number[]; modifiers?: { ctrlKey?: boolean; shiftKey?: boolean; altKey?: boolean; metaKey?: boolean; }; } /** * Deletes all selected items */ export class DeleteItemsAction extends Action { constructor(options: DeleteItemsActionOptions = {}) { const keyCodes = options.keyCodes || [46, 8]; const modifiers = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, ...options.modifiers }; super({ type: InputType.KEY_DOWN, fire: (event: ActionEvent) => { const { keyCode, ctrlKey, shiftKey, altKey, metaKey } = event.event; if (keyCodes.indexOf(keyCode) !== -1 && _isEqual({ ctrlKey, shiftKey, altKey, metaKey }, modifiers)) { _forEach(this.engine.getModel().getSelectedEntities(), (model) => { // only delete items which are not locked if (!model.isLocked()) { model.remove(); } }); this.engine.repaintCanvas(); } } }); } } ================================================ FILE: packages/react-canvas-core/src/actions/PanAndZoomCanvasAction.ts ================================================ import { WheelEvent } from 'react'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; export interface PanAndZoomCanvasActionOptions { inverseZoom?: boolean; } export class PanAndZoomCanvasAction extends Action { constructor(options: PanAndZoomCanvasActionOptions = {}) { super({ type: InputType.MOUSE_WHEEL, fire: (actionEvent: ActionEvent) => { const { event } = actionEvent; // we can block layer rendering because we are only targeting the transforms for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(false); } const model = this.engine.getModel(); event.stopPropagation(); if (event.ctrlKey) { // Pinch and zoom gesture const oldZoomFactor = this.engine.getModel().getZoomLevel() / 100; let scrollDelta = options.inverseZoom ? event.deltaY : -event.deltaY; scrollDelta /= 3; if (model.getZoomLevel() + scrollDelta > 10) { model.setZoomLevel(model.getZoomLevel() + scrollDelta); } const zoomFactor = model.getZoomLevel() / 100; const boundingRect = event.currentTarget.getBoundingClientRect(); const clientWidth = boundingRect.width; const clientHeight = boundingRect.height; // compute difference between rect before and after scroll const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor; const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor; // compute mouse coords relative to canvas const clientX = event.clientX - boundingRect.left; const clientY = event.clientY - boundingRect.top; // compute width and height increment factor const xFactor = (clientX - model.getOffsetX()) / oldZoomFactor / clientWidth; const yFactor = (clientY - model.getOffsetY()) / oldZoomFactor / clientHeight; model.setOffset(model.getOffsetX() - widthDiff * xFactor, model.getOffsetY() - heightDiff * yFactor); } else { // Pan gesture let yDelta = options.inverseZoom ? -event.deltaY : event.deltaY; let xDelta = options.inverseZoom ? -event.deltaX : event.deltaX; model.setOffset(model.getOffsetX() - xDelta, model.getOffsetY() - yDelta); } this.engine.repaintCanvas(); // re-enable rendering for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(true); } } }); } } ================================================ FILE: packages/react-canvas-core/src/actions/ZoomCanvasAction.ts ================================================ import { WheelEvent } from 'react'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; export interface ZoomCanvasActionOptions { inverseZoom?: boolean; } export class ZoomCanvasAction extends Action { constructor(options: ZoomCanvasActionOptions = {}) { super({ type: InputType.MOUSE_WHEEL, fire: (actionEvent: ActionEvent) => { const { event } = actionEvent; // we can block layer rendering because we are only targeting the transforms for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(false); } const model = this.engine.getModel(); event.stopPropagation(); const oldZoomFactor = this.engine.getModel().getZoomLevel() / 100; let scrollDelta = options.inverseZoom ? -event.deltaY : event.deltaY; //check if it is pinch gesture if (event.ctrlKey && scrollDelta % 1 !== 0) { /* Chrome and Firefox sends wheel event with deltaY that have fractional part, also `ctrlKey` prop of the event is true though ctrl isn't pressed */ scrollDelta /= 3; } else { scrollDelta /= 60; } if (model.getZoomLevel() + scrollDelta > 10) { model.setZoomLevel(model.getZoomLevel() + scrollDelta); } const zoomFactor = model.getZoomLevel() / 100; const boundingRect = event.currentTarget.getBoundingClientRect(); const clientWidth = boundingRect.width; const clientHeight = boundingRect.height; // compute difference between rect before and after scroll const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor; const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor; // compute mouse coords relative to canvas const clientX = event.clientX - boundingRect.left; const clientY = event.clientY - boundingRect.top; // compute width and height increment factor const xFactor = (clientX - model.getOffsetX()) / oldZoomFactor / clientWidth; const yFactor = (clientY - model.getOffsetY()) / oldZoomFactor / clientHeight; model.setOffset(model.getOffsetX() - widthDiff * xFactor, model.getOffsetY() - heightDiff * yFactor); this.engine.repaintCanvas(); // re-enable rendering for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(true); } } }); } } ================================================ FILE: packages/react-canvas-core/src/core/AbstractFactory.ts ================================================ import { CanvasEngine } from '../CanvasEngine'; import { FactoryBank } from './FactoryBank'; /** * Base factory for all the different types of entities. * Gets registered with the engine, and is used to generate models */ export abstract class AbstractFactory { /** * Couples the factory with the models it generates */ protected type: string; /** * The engine gets injected when the factory is registered */ protected engine: E; protected bank: FactoryBank; constructor(type: string) { this.type = type; } setDiagramEngine(engine: E) { this.engine = engine; } setFactoryBank(bank: FactoryBank) { this.bank = bank; } getType(): string { return this.type; } } ================================================ FILE: packages/react-canvas-core/src/core/AbstractModelFactory.ts ================================================ import { AbstractFactory } from './AbstractFactory'; import { BaseModel } from '../core-models/BaseModel'; import { CanvasEngine } from '../CanvasEngine'; export interface GenerateModelEvent { initialConfig?: any; } export abstract class AbstractModelFactory< T extends BaseModel = BaseModel, E extends CanvasEngine = CanvasEngine > extends AbstractFactory { /** * Generates new models (the core factory pattern) */ abstract generateModel(event: GenerateModelEvent): T; } ================================================ FILE: packages/react-canvas-core/src/core/AbstractReactFactory.tsx ================================================ import { BaseModel } from '../core-models/BaseModel'; import { AbstractModelFactory } from './AbstractModelFactory'; import { CanvasEngine } from '../CanvasEngine'; import { JSX } from 'react'; export interface GenerateWidgetEvent { model: T; } /** * Further extends the AbstractFactory to add widget generation capability. */ export abstract class AbstractReactFactory< T extends BaseModel = BaseModel, E extends CanvasEngine = CanvasEngine > extends AbstractModelFactory { /** * Generates React widgets from the model contained in the event object */ abstract generateReactWidget(event: GenerateWidgetEvent): JSX.Element; } ================================================ FILE: packages/react-canvas-core/src/core/BaseObserver.ts ================================================ import { Toolkit } from '../Toolkit'; export interface BaseEvent { firing: boolean; stopPropagation: () => any; } export interface BaseEventProxy extends BaseEvent { function: string; } /** * Listeners are always in the form of an object that contains methods that take events */ export type BaseListener = { /** * Generic event that fires before a specific event was fired */ eventWillFire?: (event: BaseEvent & { function: string }) => void; /** * Generic event that fires after a specific event was fired (even if it was consumed) */ eventDidFire?: (event: BaseEvent & { function: string }) => void; } & { /** * Type for other events that will fire */ [key: string]: (event: BaseEvent) => any; }; export interface ListenerHandle { /** * Used to degister the listener */ deregister: () => any; /** * Original ID of the listener */ id: string; /** * Original Listener */ listener: BaseListener; } /** * Base observer pattern class for working with listeners */ export class BaseObserver { protected listeners: { [id: string]: L }; constructor() { this.listeners = {}; } private fireEventInternal(fire: boolean, k: keyof L, event: BaseEvent) { this.iterateListeners((listener) => { // returning false here will instruct itteration to stop if (!fire && !event.firing) { return false; } // fire selected listener if (listener[k]) { listener[k](event as BaseEvent); } }); } fireEvent(event: Partial[0]>, k: keyof L) { event = { firing: true, stopPropagation: () => { event.firing = false; }, ...event }; // fire pre this.fireEventInternal(true, 'eventWillFire', { ...event, function: k } as BaseEventProxy); // fire main event this.fireEventInternal(false, k, event as BaseEvent); // fire post this.fireEventInternal(true, 'eventDidFire', { ...event, function: k } as BaseEventProxy); } iterateListeners(cb: (listener: L) => any) { for (let id in this.listeners) { const res = cb(this.listeners[id]); // cancel itteration on false if (res === false) { return; } } } getListenerHandle(listener: L): ListenerHandle { for (let id in this.listeners) { if (this.listeners[id] === listener) { return { id: id, listener: listener, deregister: () => { delete this.listeners[id]; } }; } } } registerListener(listener: L): ListenerHandle { const id = Toolkit.UID(); this.listeners[id] = listener; return { id: id, listener: listener, deregister: () => { delete this.listeners[id]; } }; } deregisterListener(listener: L | ListenerHandle) { if (typeof listener === 'object') { (listener as ListenerHandle).deregister(); return true; } const handle = this.getListenerHandle(listener); if (handle) { handle.deregister(); return true; } return false; } } ================================================ FILE: packages/react-canvas-core/src/core/FactoryBank.ts ================================================ import { BaseEvent, BaseListener, BaseObserver } from './BaseObserver'; import { AbstractFactory } from './AbstractFactory'; import _values from 'lodash/values'; export interface FactoryBankListener extends BaseListener { /** * Factory as added to rhe bank */ factoryAdded?: (event: BaseEvent & { factory: F }) => any; /** * Factory was removed from the bank */ factoryRemoved?: (event: BaseEvent & { factory: F }) => any; } /** * Store and managed Factories that extend from Abstractfactory */ export class FactoryBank< F extends AbstractFactory = AbstractFactory, L extends FactoryBankListener = FactoryBankListener > extends BaseObserver { protected factories: { [type: string]: F }; constructor() { super(); this.factories = {}; } getFactories(): F[] { return _values(this.factories); } clearFactories() { for (let factory in this.factories) { this.deregisterFactory(factory); } } getFactory(type: string): T { if (!this.factories[type]) { throw new Error(`Cannot find factory with type [${type}]`); } return this.factories[type] as T; } registerFactory(factory: F) { factory.setFactoryBank(this); this.factories[factory.getType()] = factory; // todo fixme this.fireEvent<'factoryAdded'>({ factory } as any, 'factoryAdded'); } deregisterFactory(type: string) { const factory = this.factories[type]; factory.setFactoryBank(null); delete this.factories[type]; // todo fixme this.fireEvent<'factoryRemoved'>({ factory } as any, 'factoryRemoved'); } } ================================================ FILE: packages/react-canvas-core/src/core/ModelGeometryInterface.ts ================================================ import { Rectangle } from '@projectstorm/geometry'; export interface ModelGeometryInterface { getBoundingBox(): Rectangle; } ================================================ FILE: packages/react-canvas-core/src/core-actions/Action.ts ================================================ import { MouseEvent, KeyboardEvent, WheelEvent, TouchEvent, SyntheticEvent } from 'react'; import { Toolkit } from '../Toolkit'; import { CanvasEngine } from '../CanvasEngine'; import { BaseModel } from '../core-models/BaseModel'; export enum InputType { MOUSE_DOWN = 'mouse-down', MOUSE_UP = 'mouse-up', MOUSE_MOVE = 'mouse-move', MOUSE_WHEEL = 'mouse-wheel', KEY_DOWN = 'key-down', KEY_UP = 'key-up', TOUCH_START = 'touch-start', TOUCH_END = 'touch-end', TOUCH_MOVE = 'touch-move' } export interface Mapping { [InputType.MOUSE_DOWN]: MouseEvent; [InputType.MOUSE_UP]: MouseEvent; [InputType.MOUSE_MOVE]: MouseEvent; [InputType.MOUSE_WHEEL]: WheelEvent; [InputType.KEY_DOWN]: KeyboardEvent; [InputType.KEY_UP]: KeyboardEvent; [InputType.TOUCH_START]: TouchEvent; [InputType.TOUCH_END]: TouchEvent; [InputType.TOUCH_MOVE]: TouchEvent; } export interface ActionEvent { event: Event; model?: Model; } export interface ActionOptions { type: InputType; fire: (event: ActionEvent) => void; } export class Action { options: ActionOptions; id: string; engine: T; constructor(options: ActionOptions) { this.options = options; this.id = Toolkit.UID(); } setEngine(engine: T) { this.engine = engine; } } ================================================ FILE: packages/react-canvas-core/src/core-actions/ActionEventBus.ts ================================================ import { Action, ActionEvent, InputType } from './Action'; import { KeyboardEvent, MouseEvent } from 'react'; import _filter from 'lodash/filter'; import _keys from 'lodash/keys'; import { CanvasEngine } from '../CanvasEngine'; import { BaseModel } from '../core-models/BaseModel'; export class ActionEventBus { protected actions: { [id: string]: Action }; protected engine: CanvasEngine; protected keys: { [key: string]: boolean }; constructor(engine: CanvasEngine) { this.actions = {}; this.engine = engine; this.keys = {}; } getKeys(): string[] { return _keys(this.keys); } registerAction(action: Action): () => void { action.setEngine(this.engine); this.actions[action.id] = action; return () => { this.deregisterAction(action); }; } deregisterAction(action: Action) { action.setEngine(null); delete this.actions[action.id]; } getActionsForType(type: InputType): Action[] { return _filter(this.actions, (action) => { return action.options.type === type; }); } getModelForEvent(actionEvent: ActionEvent): BaseModel { if (actionEvent.model) { return actionEvent.model; } return this.engine.getMouseElement(actionEvent.event); } getActionsForEvent(actionEvent: ActionEvent): Action[] { const { event } = actionEvent; if (event.type === 'mousedown') { return this.getActionsForType(InputType.MOUSE_DOWN); } else if (event.type === 'mouseup') { return this.getActionsForType(InputType.MOUSE_UP); } else if (event.type === 'keydown') { // store the recorded key this.keys[(event as KeyboardEvent).key.toLowerCase()] = true; return this.getActionsForType(InputType.KEY_DOWN); } else if (event.type === 'keyup') { // delete the recorded key delete this.keys[(event as KeyboardEvent).key.toLowerCase()]; return this.getActionsForType(InputType.KEY_UP); } else if (event.type === 'mousemove') { return this.getActionsForType(InputType.MOUSE_MOVE); } else if (event.type === 'wheel') { return this.getActionsForType(InputType.MOUSE_WHEEL); } else if (event.type === 'touchstart') { return this.getActionsForType(InputType.TOUCH_START); } else if (event.type === 'touchend') { return this.getActionsForType(InputType.TOUCH_END); } else if (event.type === 'touchmove') { return this.getActionsForType(InputType.TOUCH_MOVE); } return []; } fireAction(actionEvent: ActionEvent) { const actions = this.getActionsForEvent(actionEvent); for (let action of actions) { action.options.fire(actionEvent as any); } } } ================================================ FILE: packages/react-canvas-core/src/core-models/BaseEntity.ts ================================================ import { Toolkit } from '../Toolkit'; import _cloneDeep from 'lodash/cloneDeep'; import { CanvasEngine } from '../CanvasEngine'; import { BaseEvent, BaseListener, BaseObserver } from '../core/BaseObserver'; import { BaseModel } from './BaseModel'; export interface BaseEntityEvent extends BaseEvent { entity: T; } export interface BaseEntityListener extends BaseListener { lockChanged?(event: BaseEntityEvent & { locked: boolean }): void; } /** * @TODO move to enums */ export type BaseEntityType = 'node' | 'link' | 'port' | 'point'; export interface BaseEntityOptions { id?: string; locked?: boolean; } export type BaseEntityGenerics = { LISTENER: BaseEntityListener; OPTIONS: BaseEntityOptions; }; export interface DeserializeEvent { engine: CanvasEngine; data: ReturnType; registerModel(model: BaseModel); getModel(id: string): Promise; } export class BaseEntity extends BaseObserver { protected options: T['OPTIONS']; constructor(options: T['OPTIONS'] = {}) { super(); this.options = { id: Toolkit.UID(), ...options }; } getOptions() { return this.options; } getID() { return this.options.id; } doClone(lookupTable: { [s: string]: any } = {}, clone: any) { /*noop*/ } clone(lookupTable: { [s: string]: any } = {}) { // try and use an existing clone first if (lookupTable[this.options.id]) { return lookupTable[this.options.id]; } let clone = _cloneDeep(this); clone.options = { ...this.options, id: Toolkit.UID() }; clone.clearListeners(); lookupTable[this.options.id] = clone; this.doClone(lookupTable, clone); return clone; } clearListeners() { this.listeners = {}; } deserialize(event: DeserializeEvent) { this.options.id = event.data.id; this.options.locked = event.data.locked; } serialize() { return { id: this.options.id, locked: this.options.locked }; } fireEvent & object>(event: L, k: keyof T['LISTENER']) { super.fireEvent( { entity: this, ...event }, k ); } public isLocked(): boolean { return this.options.locked; } public setLocked(locked: boolean = true) { this.options.locked = locked; this.fireEvent( { locked: locked }, 'lockChanged' ); } } ================================================ FILE: packages/react-canvas-core/src/core-models/BaseModel.ts ================================================ import { BaseEntity, BaseEntityEvent, BaseEntityGenerics, BaseEntityListener, BaseEntityOptions, DeserializeEvent } from './BaseEntity'; import { CanvasModel } from '../entities/canvas/CanvasModel'; export interface BaseModelListener extends BaseEntityListener { selectionChanged?(event: BaseEntityEvent & { isSelected: boolean }): void; entityRemoved?(event: BaseEntityEvent): void; } export interface BaseModelOptions extends BaseEntityOptions { type?: string; selected?: boolean; extras?: any; } export interface BaseModelGenerics extends BaseEntityGenerics { LISTENER: BaseModelListener; PARENT: BaseEntity; OPTIONS: BaseModelOptions; } export class BaseModel extends BaseEntity { protected parent: G['PARENT']; constructor(options: G['OPTIONS']) { super(options); } performanceTune() { return true; } getParentCanvasModel(): CanvasModel { if (!this.parent) { return null; } if (this.parent instanceof CanvasModel) { return this.parent; } else if (this.parent instanceof BaseModel) { return this.parent.getParentCanvasModel(); } return null; } getParent(): G['PARENT'] { return this.parent; } setParent(parent: G['PARENT']) { this.parent = parent; } getSelectionEntities(): Array { return [this]; } serialize() { return { ...super.serialize(), type: this.options.type, selected: this.options.selected, extras: this.options.extras }; } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.extras = event.data.extras; this.options.selected = event.data.selected; } getType(): string { return this.options.type; } isSelected(): boolean { return this.options.selected; } isLocked(): boolean { const locked = super.isLocked(); if (locked) { return true; } // delegate this call up to the parent if (this.parent) { return this.parent.isLocked(); } return false; } setSelected(selected: boolean = true) { if (this.options.selected !== selected) { this.options.selected = selected; this.fireEvent( { isSelected: selected }, 'selectionChanged' ); } } remove() { this.fireEvent({}, 'entityRemoved'); } } ================================================ FILE: packages/react-canvas-core/src/core-models/BasePositionModel.ts ================================================ import { BaseModel, BaseModelGenerics, BaseModelListener, BaseModelOptions } from './BaseModel'; import { BaseEntityEvent, DeserializeEvent } from './BaseEntity'; import { Point, Rectangle } from '@projectstorm/geometry'; import { ModelGeometryInterface } from '../core/ModelGeometryInterface'; export interface BasePositionModelListener extends BaseModelListener { positionChanged?(event: BaseEntityEvent): void; } export interface BasePositionModelOptions extends BaseModelOptions { position?: Point; } export interface BasePositionModelGenerics extends BaseModelGenerics { LISTENER: BasePositionModelListener; OPTIONS: BasePositionModelOptions; } export class BasePositionModel extends BaseModel implements ModelGeometryInterface { protected position: Point; constructor(options: G['OPTIONS']) { super(options); this.position = options.position || new Point(0, 0); } setPosition(point: Point): void; setPosition(x: number, y: number): void; setPosition(x: number | Point, y?: number): void { if (x instanceof Point) { this.position = x; } else { this.position = new Point(x, y); } this.fireEvent({}, 'positionChanged'); } getBoundingBox(): Rectangle { return Rectangle.fromPointAndSize(this.position, 0, 0); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.position = new Point(event.data.x, event.data.y); } serialize() { return { ...super.serialize(), x: this.position.x, y: this.position.y }; } getPosition(): Point { return this.position; } getX() { return this.position.x; } getY() { return this.position.y; } } ================================================ FILE: packages/react-canvas-core/src/core-state/AbstractDisplacementState.ts ================================================ import { State, StateOptions } from './State'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { CanvasEngine } from '../CanvasEngine'; export interface AbstractDisplacementStateEvent { displacementX: number; displacementY: number; virtualDisplacementX: number; virtualDisplacementY: number; event: React.MouseEvent | React.TouchEvent; } export abstract class AbstractDisplacementState extends State { initialX: number; initialY: number; initialXRelative: number; initialYRelative: number; constructor(options: StateOptions) { super(options); this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (actionEvent: ActionEvent) => { const { clientX, clientY } = actionEvent.event; this.handleMoveStart(clientX, clientY); } }) ); this.registerAction( new Action({ type: InputType.MOUSE_MOVE, fire: (actionEvent: ActionEvent) => { const { event } = actionEvent; if (event.buttons === 0) { // If buttons is 0, it means the mouse is not down, the user may have released it // outside of the canvas, then we eject the state this.eject(); return; } const { clientX, clientY } = event; this.handleMove(clientX, clientY, event); } }) ); this.registerAction( new Action({ type: InputType.MOUSE_UP, fire: () => this.handleMoveEnd() }) ); this.registerAction( new Action({ type: InputType.TOUCH_START, fire: (actionEvent: ActionEvent) => { const { clientX, clientY } = actionEvent.event.touches[0]; this.handleMoveStart(clientX, clientY); } }) ); this.registerAction( new Action({ type: InputType.TOUCH_MOVE, fire: (actionEvent: ActionEvent) => { const { event } = actionEvent; const { clientX, clientY } = event.touches[0]; this.handleMove(clientX, clientY, event); } }) ); this.registerAction( new Action({ type: InputType.TOUCH_END, fire: () => this.handleMoveEnd() }) ); } protected handleMoveStart(x: number, y: number): void { this.initialX = x; this.initialY = y; const rel = this.engine.getRelativePoint(x, y); this.initialXRelative = rel.x; this.initialYRelative = rel.y; } protected handleMove(x: number, y: number, event: React.MouseEvent | React.TouchEvent): void { this.fireMouseMoved({ displacementX: x - this.initialX, displacementY: y - this.initialY, virtualDisplacementX: (x - this.initialX) / (this.engine.getModel().getZoomLevel() / 100.0), virtualDisplacementY: (y - this.initialY) / (this.engine.getModel().getZoomLevel() / 100.0), event }); } protected handleMoveEnd(): void { this.eject(); } abstract fireMouseMoved(event: AbstractDisplacementStateEvent); } ================================================ FILE: packages/react-canvas-core/src/core-state/State.ts ================================================ import { CanvasEngine } from '../CanvasEngine'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { SyntheticEvent } from 'react'; import _intersection from 'lodash/intersection'; export interface StateOptions { name: string; } export abstract class State { protected engine: E; protected actions: Action[]; protected keys: string[]; protected options: StateOptions; protected childStates: State[]; private handler1; private handler2; constructor(options: StateOptions) { this.actions = []; this.keys = []; this.childStates = []; this.options = options; } setEngine(engine: E) { this.engine = engine; } getOptions() { return this.options; } eject() { this.engine.getStateMachine().popState(); } transitionWithEvent(state: State, event: ActionEvent) { this.engine.getStateMachine().pushState(state); this.engine.getActionEventBus().fireAction(event); } registerAction(action: Action) { this.actions.push(action); } tryActivateParentState(keys: string[]) { if (this.keys.length > 0 && !this.isKeysFullfilled(keys)) { this.eject(); return true; } return false; } tryActivateChildState(keys: string[]) { const state = this.findStateToActivate(keys); if (state) { this.engine.getStateMachine().pushState(state); return true; } return false; } findStateToActivate(keys: string[]) { for (let child of this.childStates) { if (child.isKeysFullfilled(keys)) { return child; } } return null; } isKeysFullfilled(keys: string[]) { return _intersection(this.keys, keys).length === this.keys.length; } activated(previous: State) { const keys = this.engine.getActionEventBus().getKeys(); if (this.tryActivateParentState(keys) || this.tryActivateChildState(keys)) { return; } // perhaps we need to pop again? this.handler1 = this.engine.getActionEventBus().registerAction( new Action({ type: InputType.KEY_DOWN, fire: () => { this.tryActivateChildState(this.engine.getActionEventBus().getKeys()); } }) ); this.handler2 = this.engine.getActionEventBus().registerAction( new Action({ type: InputType.KEY_UP, fire: () => { this.tryActivateParentState(this.engine.getActionEventBus().getKeys()); } }) ); for (let action of this.actions) { this.engine.getActionEventBus().registerAction(action); } } deactivated(next: State) { if (this.handler1) { this.handler1(); } if (this.handler2) { this.handler2(); } // if this happens, we are going into heirachial state machine mode for (let action of this.actions) { this.engine.getActionEventBus().deregisterAction(action); } } } ================================================ FILE: packages/react-canvas-core/src/core-state/StateMachine.ts ================================================ import { State } from './State'; import _last from 'lodash/last'; import { CanvasEngine } from '../CanvasEngine'; import { BaseEvent, BaseListener, BaseObserver } from '../core/BaseObserver'; export interface StateMachineListener extends BaseListener { stateChanged?: (event: BaseEvent & { newState: State }) => any; } export class StateMachine extends BaseObserver { protected currentState: State; protected stateStack: State[]; protected engine: CanvasEngine; constructor(engine: CanvasEngine) { super(); this.engine = engine; this.stateStack = []; } getCurrentState() { return this.currentState; } pushState(state: State) { this.stateStack.push(state); this.setState(state); } popState() { this.stateStack.pop(); this.setState(_last(this.stateStack)); } setState(state: State) { state.setEngine(this.engine); // if no state object, get the initial state if (this.currentState) { this.currentState.deactivated(state); } const old = this.currentState; this.currentState = state; if (this.currentState) { this.currentState.activated(old); this.fireEvent<'stateChanged'>( { newState: state }, 'stateChanged' ); } } } ================================================ FILE: packages/react-canvas-core/src/entities/canvas/CanvasModel.ts ================================================ import _filter from 'lodash/filter'; import _flatMap from 'lodash/flatMap'; import _forEach from 'lodash/forEach'; import _map from 'lodash/map'; import _values from 'lodash/values'; import { BaseEntity, BaseEntityEvent, BaseEntityGenerics, BaseEntityListener, BaseEntityOptions, DeserializeEvent } from '../../core-models/BaseEntity'; import { LayerModel } from '../layer/LayerModel'; import { BaseModel } from '../../core-models/BaseModel'; import { CanvasEngine } from '../../CanvasEngine'; export interface CanvasModelListener extends BaseEntityListener { offsetUpdated?(event: BaseEntityEvent & { offsetX: number; offsetY: number }): void; zoomUpdated?(event: BaseEntityEvent & { zoom: number }): void; gridUpdated?(event: BaseEntityEvent & { size: number }): void; } export interface CanvasModelOptions extends BaseEntityOptions { offsetX?: number; offsetY?: number; zoom?: number; gridSize?: number; } export interface CanvasModelGenerics extends BaseEntityGenerics { LISTENER: CanvasModelListener; OPTIONS: CanvasModelOptions; LAYER: LayerModel; } export class CanvasModel extends BaseEntity { protected layers: G['LAYER'][]; constructor(options: G['OPTIONS'] = {}) { super({ zoom: 100, gridSize: 0, offsetX: 0, offsetY: 0, ...options }); this.layers = []; } getSelectionEntities(): BaseModel[] { return _flatMap(this.layers, (layer) => { return layer.getSelectionEntities(); }); } getSelectedEntities(): BaseModel[] { return _filter(this.getSelectionEntities(), (ob) => { return ob.isSelected(); }); } clearSelection() { _forEach(this.getSelectedEntities(), (element) => { element.setSelected(false); }); } getModels(): BaseModel[] { return _flatMap(this.layers, (layer) => { return _values(layer.getModels()); }); } addLayer(layer: LayerModel) { layer.setParent(this); layer.registerListener({ entityRemoved: (event: BaseEntityEvent): void => {} }); this.layers.push(layer); } removeLayer(layer: LayerModel) { const index = this.layers.indexOf(layer); if (index !== -1) { this.layers.splice(index, 1); return true; } return false; } getLayers() { return this.layers; } setGridSize(size: number = 0) { this.options.gridSize = size; this.fireEvent({ size: size }, 'gridUpdated'); } getGridPosition(pos: number) { if (this.options.gridSize === 0) { return pos; } return this.options.gridSize * Math.floor((pos + this.options.gridSize / 2) / this.options.gridSize); } deserializeModel(data: ReturnType, engine: CanvasEngine) { const models: { [id: string]: BaseModel; } = {}; const promises: { [id: string]: Promise; } = {}; const resolvers: { [id: string]: (model: BaseModel) => any; } = {}; const event: DeserializeEvent = { data: data, engine: engine, registerModel: (model: BaseModel) => { models[model.getID()] = model; if (resolvers[model.getID()]) { resolvers[model.getID()](model); } }, getModel(id: string): Promise { if (models[id]) { return Promise.resolve(models[id]) as Promise; } if (!promises[id]) { promises[id] = new Promise((resolve) => { resolvers[id] = resolve; }); } return promises[id] as Promise; } }; this.deserialize(event); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.offsetX = event.data.offsetX; this.options.offsetY = event.data.offsetY; this.options.zoom = event.data.zoom; this.options.gridSize = event.data.gridSize; _forEach(event.data.layers, (layer) => { const layerOb = event.engine.getFactoryForLayer(layer.type).generateModel({ initialConfig: layer }); layerOb.deserialize({ ...event, data: layer }); this.addLayer(layerOb); }); } serialize() { return { ...super.serialize(), offsetX: this.options.offsetX, offsetY: this.options.offsetY, zoom: this.options.zoom, gridSize: this.options.gridSize, layers: _map(this.layers, (layer) => { return layer.serialize(); }) }; } setZoomLevel(zoom: number) { this.options.zoom = zoom; this.fireEvent({ zoom }, 'zoomUpdated'); } setOffset(offsetX: number, offsetY: number) { this.options.offsetX = offsetX; this.options.offsetY = offsetY; this.fireEvent({ offsetX, offsetY }, 'offsetUpdated'); } setOffsetX(offsetX: number) { this.setOffset(offsetX, this.options.offsetY); } setOffsetY(offsetY: number) { this.setOffset(this.options.offsetX, offsetY); } getOffsetY() { return this.options.offsetY; } getOffsetX() { return this.options.offsetX; } getZoomLevel() { return this.options.zoom; } } ================================================ FILE: packages/react-canvas-core/src/entities/canvas/CanvasWidget.tsx ================================================ import * as React from 'react'; import { CanvasEngine } from '../../CanvasEngine'; import { TransformLayerWidget } from '../layer/TransformLayerWidget'; import styled from '@emotion/styled'; import { SmartLayerWidget } from '../layer/SmartLayerWidget'; export interface DiagramProps { engine: CanvasEngine; className?: string; } namespace S { export const Canvas = styled.div` position: relative; cursor: move; overflow: hidden; `; } export class CanvasWidget extends React.Component { ref: React.RefObject; keyUp: any; keyDown: any; canvasListener: any; constructor(props: DiagramProps) { super(props); this.ref = React.createRef(); this.state = { action: null, diagramEngineListener: null }; } componentWillUnmount() { this.props.engine.deregisterListener(this.canvasListener); this.props.engine.setCanvas(null); document.removeEventListener('keyup', this.keyUp); document.removeEventListener('keydown', this.keyDown); } registerCanvas() { this.props.engine.setCanvas(this.ref.current); this.props.engine.iterateListeners((list) => { list.rendered && list.rendered(); }); } componentDidUpdate() { this.registerCanvas(); } componentDidMount() { this.canvasListener = this.props.engine.registerListener({ repaintCanvas: () => { this.forceUpdate(); } }); this.keyDown = (event) => { this.props.engine.getActionEventBus().fireAction({ event }); }; this.keyUp = (event) => { this.props.engine.getActionEventBus().fireAction({ event }); }; document.addEventListener('keyup', this.keyUp); document.addEventListener('keydown', this.keyDown); this.registerCanvas(); } render() { const engine = this.props.engine; const model = engine.getModel(); return ( { this.props.engine.getActionEventBus().fireAction({ event }); }} onMouseDown={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} onMouseUp={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} onMouseMove={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} onTouchStart={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} onTouchEnd={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} onTouchMove={(event) => { this.props.engine.getActionEventBus().fireAction({ event }); }} > {model.getLayers().map((layer) => { return ( ); })} ); } } ================================================ FILE: packages/react-canvas-core/src/entities/layer/LayerModel.ts ================================================ import { BaseModel, BaseModelGenerics, BaseModelOptions } from '../../core-models/BaseModel'; import { CanvasModel } from '../canvas/CanvasModel'; import _flatMap from 'lodash/flatMap'; import _forEach from 'lodash/forEach'; import _mapValues from 'lodash/mapValues'; import { CanvasEngine } from '../../CanvasEngine'; import { FactoryBank } from '../../core/FactoryBank'; import { AbstractModelFactory } from '../../core/AbstractModelFactory'; import { DeserializeEvent } from '../../core-models/BaseEntity'; export interface LayerModelOptions extends BaseModelOptions { isSvg?: boolean; transformed?: boolean; } export interface LayerModelGenerics extends BaseModelGenerics { OPTIONS: LayerModelOptions; PARENT: CanvasModel; CHILDREN: BaseModel; ENGINE: CanvasEngine; } export abstract class LayerModel extends BaseModel { protected models: { [id: string]: G['CHILDREN'] }; protected repaintEnabled: boolean; constructor(options: G['OPTIONS'] = {}) { super(options); this.models = {}; this.repaintEnabled = true; } /** * This is used for deserialization */ abstract getChildModelFactoryBank(engine: G['ENGINE']): FactoryBank>; deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.isSvg = !!event.data.isSvg; this.options.transformed = !!event.data.transformed; _forEach(event.data.models, (model) => { const modelOb = this.getChildModelFactoryBank(event.engine).getFactory(model.type).generateModel({ initialConfig: model }); modelOb.deserialize({ ...event, data: model }); this.addModel(modelOb); }); } serialize() { return { ...super.serialize(), isSvg: this.options.isSvg, transformed: this.options.transformed, models: _mapValues(this.models, (model) => { return model.serialize(); }) }; } isRepaintEnabled() { return this.repaintEnabled; } allowRepaint(allow: boolean = true) { this.repaintEnabled = allow; } remove() { if (this.parent) { this.parent.removeLayer(this); } super.remove(); } addModel(model: G['CHILDREN']) { model.setParent(this); this.models[model.getID()] = model; } getSelectionEntities(): Array { return _flatMap(this.models, (model) => { return model.getSelectionEntities(); }); } getModels() { return this.models; } getModel(id: string) { return this.models[id]; } removeModel(id: string | G['CHILDREN']): boolean { const _id = typeof id === 'string' ? id : id.getID(); if (this.models[_id]) { delete this.models[_id]; return true; } return false; } } ================================================ FILE: packages/react-canvas-core/src/entities/layer/SmartLayerWidget.tsx ================================================ import * as React from 'react'; import { LayerModel } from './LayerModel'; import { CanvasEngine } from '../../CanvasEngine'; export interface SmartLayerWidgetProps { layer: LayerModel; engine: CanvasEngine; } export class SmartLayerWidget extends React.Component { shouldComponentUpdate(): boolean { return this.props.layer.isRepaintEnabled(); } render() { return this.props.engine.getFactoryForLayer(this.props.layer).generateReactWidget({ model: this.props.layer }); } } ================================================ FILE: packages/react-canvas-core/src/entities/layer/TransformLayerWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; import { CSSProperties } from 'react'; import { LayerModel } from './LayerModel'; import { css } from '@emotion/react'; export interface TransformLayerWidgetProps { layer: LayerModel; } namespace S { const shared = css` top: 0; left: 0; right: 0; bottom: 0; position: absolute; pointer-events: none; transform-origin: 0 0; width: 100%; height: 100%; overflow: visible; `; export const DivLayer = styled.div` ${shared} `; export const SvgLayer = styled.svg` ${shared} `; } export class TransformLayerWidget extends React.Component> { constructor(props: TransformLayerWidgetProps) { super(props); this.state = {}; } getTransform() { const model = this.props.layer.getParent(); return ` translate( ${model.getOffsetX()}px, ${model.getOffsetY()}px) scale( ${model.getZoomLevel() / 100.0} ) `; } getTransformStyle(): CSSProperties { if (this.props.layer.getOptions().transformed) { return { transform: this.getTransform() }; } return {}; } render() { if (this.props.layer.getOptions().isSvg) { return {this.props.children}; } return {this.props.children}; } } ================================================ FILE: packages/react-canvas-core/src/entities/selection/SelectionBoxLayerFactory.tsx ================================================ import * as React from 'react'; import { AbstractReactFactory, GenerateWidgetEvent } from '../../core/AbstractReactFactory'; import { SelectionLayerModel } from './SelectionLayerModel'; import { GenerateModelEvent } from '../../core/AbstractModelFactory'; import { SelectionBoxWidget } from './SelectionBoxWidget'; import { JSX } from 'react'; export class SelectionBoxLayerFactory extends AbstractReactFactory { constructor() { super('selection'); } generateModel(event: GenerateModelEvent): SelectionLayerModel { return new SelectionLayerModel(); } generateReactWidget(event: GenerateWidgetEvent): JSX.Element { return ; } } ================================================ FILE: packages/react-canvas-core/src/entities/selection/SelectionBoxWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; import { SimpleClientRect } from '../../states/SelectionBoxState'; export interface SelectionBoxWidgetProps { rect: SimpleClientRect; } namespace S { export const Container = styled.div` position: absolute; background-color: rgba(0, 192, 255, 0.2); border: solid 2px rgb(0, 192, 255); `; } export class SelectionBoxWidget extends React.Component { render() { const { rect } = this.props; if (!rect) return null; return ( ); } } ================================================ FILE: packages/react-canvas-core/src/entities/selection/SelectionLayerModel.ts ================================================ import { LayerModel } from '../layer/LayerModel'; import { FactoryBank } from '../../core/FactoryBank'; import { AbstractModelFactory } from '../../core/AbstractModelFactory'; import { BaseModel } from '../../core-models/BaseModel'; import { SimpleClientRect } from '../../states/SelectionBoxState'; export class SelectionLayerModel extends LayerModel { box: SimpleClientRect; constructor() { super({ transformed: false, isSvg: false, type: 'selection' }); } setBox(rect: SimpleClientRect) { this.box = rect; } getChildModelFactoryBank(): FactoryBank> { // is not used as it doesnt serialize return null; } } ================================================ FILE: packages/react-canvas-core/src/index.ts ================================================ export * from './CanvasEngine'; export * from './Toolkit'; export * from './entities/canvas/CanvasModel'; export * from './core/AbstractFactory'; export * from './core/AbstractModelFactory'; export * from './core/AbstractReactFactory'; export * from './core/BaseObserver'; export * from './core/FactoryBank'; export * from './core/ModelGeometryInterface'; export * from './core-actions/Action'; export * from './core-actions/ActionEventBus'; export * from './core-models/BaseEntity'; export * from './core-models/BaseModel'; export * from './core-models/BasePositionModel'; export * from './entities/canvas/CanvasModel'; export * from './entities/canvas/CanvasWidget'; export * from './entities/layer/LayerModel'; export * from './entities/layer/TransformLayerWidget'; export * from './entities/layer/SmartLayerWidget'; export * from './entities/selection/SelectionBoxLayerFactory'; export * from './entities/selection/SelectionBoxWidget'; export * from './entities/selection/SelectionLayerModel'; export * from './widgets/PeformanceWidget'; export * from './core-state/AbstractDisplacementState'; export * from './core-state/State'; export * from './core-state/StateMachine'; export * from './states/DefaultState'; export * from './states/DragCanvasState'; export * from './states/SelectingState'; export * from './states/SelectionBoxState'; export * from './states/MoveItemsState'; export * from './actions/DeleteItemsAction'; export * from './actions/ZoomCanvasAction'; export * from './actions/PanAndZoomCanvasAction'; ================================================ FILE: packages/react-canvas-core/src/states/DefaultState.ts ================================================ import { State } from '../core-state/State'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { MouseEvent, TouchEvent } from 'react'; import { DragCanvasState } from './DragCanvasState'; import { SelectingState } from './SelectingState'; import { MoveItemsState } from './MoveItemsState'; export class DefaultState extends State { constructor() { super({ name: 'default' }); this.childStates = [new SelectingState()]; // determine what was clicked on this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); // the canvas was clicked on, transition to the dragging canvas state if (!element) { this.transitionWithEvent(new DragCanvasState(), event); } else { this.transitionWithEvent(new MoveItemsState(), event); } } }) ); // touch drags the canvas this.registerAction( new Action({ type: InputType.TOUCH_START, fire: (event: ActionEvent) => { this.transitionWithEvent(new DragCanvasState(), event); } }) ); } } ================================================ FILE: packages/react-canvas-core/src/states/DragCanvasState.ts ================================================ import { CanvasEngine } from '../CanvasEngine'; import { AbstractDisplacementState, AbstractDisplacementStateEvent } from '../core-state/AbstractDisplacementState'; import { State } from '../core-state/State'; export interface DragCanvasStateOptions { /** * If enabled, the canvas is available to drag */ allowDrag?: boolean; } export class DragCanvasState extends AbstractDisplacementState { // store this as we drag the canvas initialCanvasX: number; initialCanvasY: number; config: DragCanvasStateOptions; constructor(options: DragCanvasStateOptions = {}) { super({ name: 'drag-canvas' }); this.config = { allowDrag: true, ...options }; } async activated(prev) { super.activated(prev); this.engine.getModel().clearSelection(); await this.engine.repaintCanvas(true); // we can block layer rendering because we are only targeting the transforms for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(false); } this.initialCanvasX = this.engine.getModel().getOffsetX(); this.initialCanvasY = this.engine.getModel().getOffsetY(); } deactivated(next: State) { super.deactivated(next); for (let layer of this.engine.getModel().getLayers()) { layer.allowRepaint(true); } } fireMouseMoved(event: AbstractDisplacementStateEvent) { if (this.config.allowDrag) { this.engine .getModel() .setOffset(this.initialCanvasX + event.displacementX, this.initialCanvasY + event.displacementY); this.engine.repaintCanvas(); } } } ================================================ FILE: packages/react-canvas-core/src/states/MoveItemsState.ts ================================================ import { AbstractDisplacementState, AbstractDisplacementStateEvent } from '../core-state/AbstractDisplacementState'; import { State } from '../core-state/State'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { BasePositionModel } from '../core-models/BasePositionModel'; import { Point } from '@projectstorm/geometry'; import { CanvasEngine } from '../CanvasEngine'; export class MoveItemsState extends AbstractDisplacementState { initialPositions: { [id: string]: { point: Point; item: BasePositionModel; }; }; constructor() { super({ name: 'move-items' }); this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); if (!element) { return; } if (!element.isSelected()) { this.engine.getModel().clearSelection(); } element.setSelected(true); this.engine.repaintCanvas(); } }) ); } activated(previous: State) { super.activated(previous); this.initialPositions = {}; } fireMouseMoved(event: AbstractDisplacementStateEvent) { const items = this.engine.getModel().getSelectedEntities(); const model = this.engine.getModel(); for (let item of items) { if (item instanceof BasePositionModel) { if (item.isLocked()) { continue; } if (!this.initialPositions[item.getID()]) { this.initialPositions[item.getID()] = { point: item.getPosition(), item: item }; } const pos = this.initialPositions[item.getID()].point; item.setPosition( model.getGridPosition(pos.x + event.virtualDisplacementX), model.getGridPosition(pos.y + event.virtualDisplacementY) ); } } this.engine.repaintCanvas(); } } ================================================ FILE: packages/react-canvas-core/src/states/SelectingState.ts ================================================ import { State } from '../core-state/State'; import { Action, ActionEvent, InputType } from '../core-actions/Action'; import { SelectionBoxState } from './SelectionBoxState'; import { MouseEvent } from 'react'; import { CanvasEngine } from '../CanvasEngine'; export class SelectingState extends State { constructor() { super({ name: 'selecting' }); this.keys = ['shift']; this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); // go into a selection box on the canvas state if (!element) { this.transitionWithEvent(new SelectionBoxState(), event); } else { element.setSelected(true); this.engine.repaintCanvas(); } } }) ); } } ================================================ FILE: packages/react-canvas-core/src/states/SelectionBoxState.ts ================================================ import { AbstractDisplacementState, AbstractDisplacementStateEvent } from '../core-state/AbstractDisplacementState'; import { State } from '../core-state/State'; import { SelectionLayerModel } from '../entities/selection/SelectionLayerModel'; import { Point, Rectangle } from '@projectstorm/geometry'; import { BasePositionModel } from '../core-models/BasePositionModel'; import { ModelGeometryInterface } from '../core/ModelGeometryInterface'; import { CanvasEngine } from '../CanvasEngine'; export interface SimpleClientRect { left: number; right: number; width: number; height: number; top: number; bottom: number; } export class SelectionBoxState extends AbstractDisplacementState { layer: SelectionLayerModel; constructor() { super({ name: 'selection-box' }); } activated(previous: State) { super.activated(previous); this.layer = new SelectionLayerModel(); this.engine.getModel().addLayer(this.layer); } deactivated(next: State) { super.deactivated(next); this.layer.remove(); this.engine.repaintCanvas(); } getBoxDimensions(event: AbstractDisplacementStateEvent): SimpleClientRect { let rel: Point; if ('touches' in event.event) { const touch = event.event.touches[0]; rel = this.engine.getRelativePoint(touch.clientX, touch.clientY); } else { rel = this.engine.getRelativePoint(event.event.clientX, event.event.clientY); } return { left: rel.x > this.initialXRelative ? this.initialXRelative : rel.x, top: rel.y > this.initialYRelative ? this.initialYRelative : rel.y, width: Math.abs(rel.x - this.initialXRelative), height: Math.abs(rel.y - this.initialYRelative), right: rel.x < this.initialXRelative ? this.initialXRelative : rel.x, bottom: rel.y < this.initialYRelative ? this.initialYRelative : rel.y }; } fireMouseMoved(event: AbstractDisplacementStateEvent) { this.layer.setBox(this.getBoxDimensions(event)); const relative = this.engine.getRelativeMousePoint({ clientX: this.initialX, clientY: this.initialY }); if (event.virtualDisplacementX < 0) { relative.x -= Math.abs(event.virtualDisplacementX); } if (event.virtualDisplacementY < 0) { relative.y -= Math.abs(event.virtualDisplacementY); } const rect = Rectangle.fromPointAndSize( relative, Math.abs(event.virtualDisplacementX), Math.abs(event.virtualDisplacementY) ); for (let model of this.engine.getModel().getSelectionEntities()) { if ((model as unknown as ModelGeometryInterface).getBoundingBox) { const bounds = (model as unknown as ModelGeometryInterface).getBoundingBox(); if (rect.containsPoint(bounds.getTopLeft()) && rect.containsPoint(bounds.getBottomRight())) { model.setSelected(true); } else { model.setSelected(false); } } } this.engine.repaintCanvas(); } } ================================================ FILE: packages/react-canvas-core/src/widgets/PeformanceWidget.tsx ================================================ import * as React from 'react'; import _isEqual from 'lodash/isEqual'; import { BaseModel } from '../core-models/BaseModel'; import { JSX } from 'react'; export interface PeformanceWidgetProps { children: () => JSX.Element; serialized: object; model: BaseModel; } export interface PeformanceWidgetState {} export class PeformanceWidget extends React.Component { shouldComponentUpdate( nextProps: Readonly, nextState: Readonly, nextContext: any ): boolean { if (!this.props.model.performanceTune()) { return true; } // deserialization event if (this.props.model !== nextProps.model) { return true; } // change event return !_isEqual(this.props.serialized, nextProps.serialized); } render() { return this.props.children(); } } ================================================ FILE: packages/react-canvas-core/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "outDir": "dist", "rootDir": "src", "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ { "path": "../geometry" } ] } ================================================ FILE: packages/react-canvas-core/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: 'projectstorm/react-canvas-core' } }; ================================================ FILE: packages/react-diagrams/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/react-diagrams/CHANGELOG.md ================================================ # @projectstorm/react-diagrams ## 7.0.4 ### Patch Changes - Updated dependencies [09ed60f] - Updated dependencies [20766f5] - Updated dependencies [80285fe] - @projectstorm/react-canvas-core@7.0.3 - @projectstorm/react-diagrams-core@7.0.3 - @projectstorm/react-diagrams-defaults@7.1.3 - @projectstorm/react-diagrams-routing@7.1.3 ## 7.0.3 ### Patch Changes - Updated dependencies [66c687a] - @projectstorm/react-diagrams-defaults@7.1.2 - @projectstorm/react-diagrams-routing@7.1.2 - @projectstorm/react-diagrams-core@7.0.2 - @projectstorm/react-canvas-core@7.0.2 ## 7.0.2 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/react-canvas-core@7.0.1 - @projectstorm/react-diagrams-core@7.0.1 - @projectstorm/react-diagrams-defaults@7.1.1 - @projectstorm/react-diagrams-routing@7.1.1 ## 7.0.1 ### Patch Changes - Updated dependencies [e0d21f1] - @projectstorm/react-diagrams-defaults@7.1.0 - @projectstorm/react-diagrams-routing@7.1.0 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-diagrams-defaults@7.0.0 - @projectstorm/react-diagrams-routing@7.0.0 - @projectstorm/react-diagrams-core@7.0.0 - @projectstorm/react-canvas-core@7.0.0 ================================================ FILE: packages/react-diagrams/README.md ================================================ ================================================ FILE: packages/react-diagrams/package.json ================================================ { "name": "@projectstorm/react-diagrams", "version": "7.0.4", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@projectstorm/react-canvas-core": "workspace:*", "@projectstorm/react-diagrams-core": "workspace:*", "@projectstorm/react-diagrams-defaults": "workspace:*", "@projectstorm/react-diagrams-routing": "workspace:*" } } ================================================ FILE: packages/react-diagrams/src/index.ts ================================================ import { DefaultDiagramState, DiagramEngine, LinkLayerFactory, NodeLayerFactory } from '@projectstorm/react-diagrams-core'; import { DefaultLabelFactory, DefaultLinkFactory, DefaultNodeFactory, DefaultPortFactory } from '@projectstorm/react-diagrams-defaults'; import { PathFindingLinkFactory } from '@projectstorm/react-diagrams-routing'; import { SelectionBoxLayerFactory, CanvasEngineOptions } from '@projectstorm/react-canvas-core'; export * from '@projectstorm/react-canvas-core'; export * from '@projectstorm/react-diagrams-core'; export * from '@projectstorm/react-diagrams-defaults'; export * from '@projectstorm/react-diagrams-routing'; /** * Construct an engine with the defaults installed */ export default (options: CanvasEngineOptions = {}): DiagramEngine => { const engine = new DiagramEngine(options); // register model factories engine.getLayerFactories().registerFactory(new NodeLayerFactory()); engine.getLayerFactories().registerFactory(new LinkLayerFactory()); engine.getLayerFactories().registerFactory(new SelectionBoxLayerFactory()); engine.getLabelFactories().registerFactory(new DefaultLabelFactory()); engine.getNodeFactories().registerFactory(new DefaultNodeFactory()); // i cant figure out why engine.getLinkFactories().registerFactory(new DefaultLinkFactory()); engine.getLinkFactories().registerFactory(new PathFindingLinkFactory()); engine.getPortFactories().registerFactory(new DefaultPortFactory()); // register the default interaction behaviours engine.getStateMachine().pushState(new DefaultDiagramState()); return engine; }; ================================================ FILE: packages/react-diagrams/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ { "path": "../react-canvas-core" }, { "path": "../react-diagrams-defaults" }, { "path": "../react-diagrams-routing" } ] } ================================================ FILE: packages/react-diagrams/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: 'projectstorm/react-diagrams' } }; ================================================ FILE: packages/react-diagrams-core/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/react-diagrams-core/CHANGELOG.md ================================================ # Changelog ## 7.0.3 ### Patch Changes - 09ed60f: Allow more derived State classes to provide a generic type - 80285fe: refactor: update lodash imports to use individual functions - Updated dependencies [09ed60f] - Updated dependencies [80285fe] - @projectstorm/react-canvas-core@7.0.3 - @projectstorm/geometry@7.0.3 ## 7.0.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - Updated dependencies [66c687a] - @projectstorm/react-canvas-core@7.0.2 - @projectstorm/geometry@7.0.2 ## 7.0.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/geometry@7.0.1 - @projectstorm/react-canvas-core@7.0.1 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-canvas-core@7.0.0 - @projectstorm/geometry@7.0.0 ## 6.0.0 ### Breaking changes - `AbstractFactory:getNewInstance` renamed to `AbstractFactory:generateModel` and now gets given an event object so that we can add to the event object without relying on more parameters - `AbstractFactory::generateReactWidget` now receives an event object - Moved factories in the diagramEngine into `FactoryBank`'s, which means we can remove the listeners in the DiagramEngine. methods such as factoryAdded and factoryRemoved are now available on the FactoryBank (better design that allows more control) - `addListener` renamed to `registerListener` ================================================ FILE: packages/react-diagrams-core/README.md ================================================ # Project STORM > React Diagrams > Core This workspace houses the default models ================================================ FILE: packages/react-diagrams-core/package.json ================================================ { "name": "@projectstorm/react-diagrams-core", "version": "7.0.3", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack", "build:es": "../../node_modules/.bin/tsc", "build:prod": "NODE_ENV=production ../../node_modules/.bin/webpack && NODE_ENV=production ../../node_modules/.bin/tsc" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@emotion/styled": "^11.11.0", "@projectstorm/geometry": "workspace:*", "@projectstorm/react-canvas-core": "workspace:*", "lodash": "^4.17.21", "react": "^19.0.0", "resize-observer-polyfill": "^1.5.1" }, "devDependencies": { "@types/lodash": "^4.14.200", "@types/react": "^19.0.12" } } ================================================ FILE: packages/react-diagrams-core/src/DiagramEngine.ts ================================================ import { NodeModel } from './entities/node/NodeModel'; import { PortModel } from './entities/port/PortModel'; import { LinkModel } from './entities/link/LinkModel'; import { LabelModel } from './entities/label/LabelModel'; import { boundingBoxFromPolygons, Point, Rectangle } from '@projectstorm/geometry'; import { JSX, MouseEvent } from 'react'; import { AbstractModelFactory, AbstractReactFactory, BaseModel, CanvasEngine, CanvasEngineListener, CanvasEngineOptions, FactoryBank, Toolkit } from '@projectstorm/react-canvas-core'; import { DiagramModel } from './models/DiagramModel'; /** * Passed as a parameter to the DiagramWidget */ export class DiagramEngine extends CanvasEngine { protected nodeFactories: FactoryBank>; protected linkFactories: FactoryBank>; protected portFactories: FactoryBank>; protected labelFactories: FactoryBank>; maxNumberPointsPerLink: number; constructor(options: CanvasEngineOptions = {}) { super(options); this.maxNumberPointsPerLink = 1000; // create banks for the different factory types this.nodeFactories = new FactoryBank(); this.linkFactories = new FactoryBank(); this.portFactories = new FactoryBank(); this.labelFactories = new FactoryBank(); const setup = (factory: FactoryBank) => { factory.registerListener({ factoryAdded: (event) => { event.factory.setDiagramEngine(this); }, factoryRemoved: (event) => { event.factory.setDiagramEngine(null); } }); }; setup(this.nodeFactories); setup(this.linkFactories); setup(this.portFactories); setup(this.labelFactories); } /** * Gets a model and element under the mouse cursor */ getMouseElement(event: MouseEvent): BaseModel { var target = event.target as Element; var diagramModel = this.model; //is it a port var element = Toolkit.closest(target, '.port[data-name]'); if (element) { var nodeElement = Toolkit.closest(target, '.node[data-nodeid]') as HTMLElement; return diagramModel.getNode(nodeElement.getAttribute('data-nodeid')).getPort(element.getAttribute('data-name')); } //look for a point element = Toolkit.closest(target, '.point[data-id]'); if (element) { return diagramModel.getLink(element.getAttribute('data-linkid')).getPointModel(element.getAttribute('data-id')); } //look for a link element = Toolkit.closest(target, '[data-linkid]'); if (element) { return diagramModel.getLink(element.getAttribute('data-linkid')); } //look for a node element = Toolkit.closest(target, '.node[data-nodeid]'); if (element) { return diagramModel.getNode(element.getAttribute('data-nodeid')); } return null; } //!-------------- FACTORIES ------------ getNodeFactories() { return this.nodeFactories; } getLinkFactories() { return this.linkFactories; } getLabelFactories() { return this.labelFactories; } getPortFactories() { return this.portFactories; } getFactoryForNode>(node: NodeModel | string) { if (typeof node === 'string') { return this.nodeFactories.getFactory(node); } return this.nodeFactories.getFactory(node.getType()); } getFactoryForLink>(link: LinkModel | string) { if (typeof link === 'string') { return this.linkFactories.getFactory(link); } return this.linkFactories.getFactory(link.getType()); } getFactoryForLabel>(label: LabelModel) { if (typeof label === 'string') { return this.labelFactories.getFactory(label); } return this.labelFactories.getFactory(label.getType()); } getFactoryForPort>(port: PortModel) { if (typeof port === 'string') { return this.portFactories.getFactory(port); } return this.portFactories.getFactory(port.getType()); } generateWidgetForLink(link: LinkModel): JSX.Element { return this.getFactoryForLink(link).generateReactWidget({ model: link }); } generateWidgetForNode(node: NodeModel): JSX.Element { return this.getFactoryForNode(node).generateReactWidget({ model: node }); } getNodeElement(node: NodeModel): Element { const selector = this.canvas.querySelector(`.node[data-nodeid="${node.getID()}"]`); if (selector === null) { throw new Error('Cannot find Node element with nodeID: [' + node.getID() + ']'); } return selector; } getNodePortElement(port: PortModel): any { var selector = this.canvas.querySelector( `.port[data-name="${port.getName()}"][data-nodeid="${port.getParent().getID()}"]` ); if (selector === null) { throw new Error( 'Cannot find Node Port element with nodeID: [' + port.getParent().getID() + '] and name: [' + port.getName() + ']' ); } return selector; } getPortCenter(port: PortModel): Point { return this.getPortCoords(port).getOrigin(); } /** * Calculate rectangular coordinates of the port passed in. */ getPortCoords(port: PortModel, element?: HTMLDivElement): Rectangle { if (!this.canvas) { throw new Error('Canvas needs to be set first'); } if (!element) { element = this.getNodePortElement(port); } const sourceRect = element.getBoundingClientRect(); const point = this.getRelativeMousePoint({ clientX: sourceRect.left, clientY: sourceRect.top }); const zoom = this.model.getZoomLevel() / 100.0; return Rectangle.fromPointAndSize(point, sourceRect.width / zoom, sourceRect.height / zoom); } /** * Determine the width and height of the node passed in. * It currently assumes nodes have a rectangular shape, can be overriden for customised shapes. */ getNodeDimensions(node: NodeModel): { width: number; height: number } { if (!this.canvas) { return { width: 0, height: 0 }; } const nodeElement = this.getNodeElement(node); const nodeRect = nodeElement.getBoundingClientRect(); return { width: nodeRect.width, height: nodeRect.height }; } getBoundingNodesRect(nodes: NodeModel[]): Rectangle { if (nodes) { if (nodes.length === 0) { return new Rectangle(); } return new Rectangle(boundingBoxFromPolygons(nodes.map((node) => node.getBoundingBox()))); } } zoomToFitSelectedNodes(options: { margin?: number; maxZoom?: number }) { const nodes: NodeModel[] = this.model .getSelectedEntities() .filter((entity) => entity instanceof NodeModel) as NodeModel[]; this.zoomToFitNodes({ margin: options.margin, maxZoom: options.maxZoom, nodes: nodes.length > 0 ? nodes : null }); } zoomToFitNodes(options: { margin?: number; nodes?: NodeModel[]; maxZoom?: number }); /** * @deprecated */ zoomToFitNodes(margin: number); zoomToFitNodes(options) { let margin = options || 0; let nodes: NodeModel[] = []; let maxZoom: number | null = null; if (!!options && typeof options == 'object') { margin = options.margin || 0; nodes = options.nodes || []; maxZoom = options.maxZoom || null; } // no node selected if (nodes.length === 0) { nodes = this.model.getNodes(); } const nodesRect = this.getBoundingNodesRect(nodes); if (nodesRect) { // there is something we should zoom on let canvasRect = this.canvas.getBoundingClientRect(); const calculate = (margin: number = 0) => { // work out zoom const xFactor = this.canvas.clientWidth / (nodesRect.getWidth() + margin * 2); const yFactor = this.canvas.clientHeight / (nodesRect.getHeight() + margin * 2); let zoomFactor = xFactor < yFactor ? xFactor : yFactor; if (maxZoom && zoomFactor > maxZoom) { zoomFactor = maxZoom; } return { zoom: zoomFactor, x: canvasRect.width / 2 - ((nodesRect.getWidth() + margin * 2) / 2 + nodesRect.getTopLeft().x) * zoomFactor + margin, y: canvasRect.height / 2 - ((nodesRect.getHeight() + margin * 2) / 2 + nodesRect.getTopLeft().y) * zoomFactor + margin }; }; let params = calculate(0); if (margin) { if (params.x < margin || params.y < margin) { params = calculate(margin); } } // apply this.model.setZoomLevel(params.zoom * 100); this.model.setOffset(params.x, params.y); this.repaintCanvas(); } } getMaxNumberPointsPerLink(): number { return this.maxNumberPointsPerLink; } setMaxNumberPointsPerLink(max: number) { this.maxNumberPointsPerLink = max; } } ================================================ FILE: packages/react-diagrams-core/src/entities/label/LabelModel.ts ================================================ import { LinkModel } from '../link/LinkModel'; import { BaseModel, BaseModelGenerics, BaseModelOptions, DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface LabelModelOptions extends BaseModelOptions { offsetX?: number; offsetY?: number; } export interface LabelModelGenerics extends BaseModelGenerics { PARENT: LinkModel; OPTIONS: LabelModelOptions; } export class LabelModel extends BaseModel { constructor(options: G['OPTIONS']) { super({ ...options, offsetX: options.offsetX || 0, offsetY: options.offsetY || 0 }); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.offsetX = event.data.offsetX; this.options.offsetY = event.data.offsetY; } serialize() { return { ...super.serialize(), offsetX: this.options.offsetX, offsetY: this.options.offsetY }; } } ================================================ FILE: packages/react-diagrams-core/src/entities/label/LabelWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine } from '../../DiagramEngine'; import { LabelModel } from './LabelModel'; import styled from '@emotion/styled'; export interface LabelWidgetProps { engine: DiagramEngine; label: LabelModel; index: number; } namespace S { export const Label = styled.div` display: inline-block; position: absolute; `; export const Foreign = styled.foreignObject` pointer-events: none; overflow: visible; `; } export class LabelWidget extends React.Component { ref: React.RefObject; constructor(props: LabelWidgetProps) { super(props); this.ref = React.createRef(); } componentDidUpdate() { window.requestAnimationFrame(this.calculateLabelPosition); } componentDidMount() { window.requestAnimationFrame(this.calculateLabelPosition); } findPathAndRelativePositionToRenderLabel = (index: number): { path: SVGPathElement; position: number } => { // an array to hold all path lengths, making sure we hit the DOM only once to fetch this information const link = this.props.label.getParent(); const lengths = link.getRenderedPath().map((path) => path.getTotalLength()); // calculate the point where we want to display the label let labelPosition = lengths.reduce((previousValue, currentValue) => previousValue + currentValue, 0) * (index / (link.getLabels().length + 1)); // find the path where the label will be rendered and calculate the relative position let pathIndex = 0; while (pathIndex < link.getRenderedPath().length) { if (labelPosition - lengths[pathIndex] < 0) { return { path: link.getRenderedPath()[pathIndex], position: labelPosition }; } // keep searching labelPosition -= lengths[pathIndex]; pathIndex++; } }; calculateLabelPosition = () => { const found = this.findPathAndRelativePositionToRenderLabel(this.props.index + 1); if (!found) { return; } const { path, position } = found; const labelDimensions = { width: this.ref.current.offsetWidth, height: this.ref.current.offsetHeight }; const pathCentre = path.getPointAtLength(position); const labelCoordinates = { x: pathCentre.x - labelDimensions.width / 2 + this.props.label.getOptions().offsetX, y: pathCentre.y - labelDimensions.height / 2 + this.props.label.getOptions().offsetY }; this.ref.current.style.transform = `translate(${labelCoordinates.x}px, ${labelCoordinates.y}px)`; }; render() { const canvas = this.props.engine.getCanvas(); return ( {this.props.engine.getFactoryForLabel(this.props.label).generateReactWidget({ model: this.props.label })} ); } } ================================================ FILE: packages/react-diagrams-core/src/entities/link/LinkModel.ts ================================================ import { PortModel } from '../port/PortModel'; import { PointModel } from './PointModel'; import _forEach from 'lodash/forEach'; import _map from 'lodash/map'; import _slice from 'lodash/slice'; import { LabelModel } from '../label/LabelModel'; import { DiagramEngine } from '../../DiagramEngine'; import { DiagramModel } from '../../models/DiagramModel'; import { boundingBoxFromPoints, Point, Rectangle } from '@projectstorm/geometry'; import { BaseEntityEvent, BaseModel, BaseModelGenerics, BaseModelListener, DeserializeEvent, ModelGeometryInterface } from '@projectstorm/react-canvas-core'; export interface LinkModelListener extends BaseModelListener { sourcePortChanged?(event: BaseEntityEvent & { port: null | PortModel }): void; targetPortChanged?(event: BaseEntityEvent & { port: null | PortModel }): void; } export interface LinkModelGenerics extends BaseModelGenerics { LISTENER: LinkModelListener; PARENT: DiagramModel; } export class LinkModel extends BaseModel implements ModelGeometryInterface { protected sourcePort: PortModel | null; protected targetPort: PortModel | null; protected labels: LabelModel[]; protected points: PointModel[]; protected renderedPaths: SVGPathElement[]; constructor(options: G['OPTIONS']) { super(options); this.points = [ new PointModel({ link: this }), new PointModel({ link: this }) ]; this.sourcePort = null; this.targetPort = null; this.renderedPaths = []; this.labels = []; } getBoundingBox(): Rectangle { return new Rectangle( boundingBoxFromPoints( _map(this.points, (point: PointModel) => { return point.getPosition(); }) ) ); } getSelectionEntities(): Array { if (this.getTargetPort() && this.getSourcePort()) { return super.getSelectionEntities().concat(_slice(this.points, 1, this.points.length - 1)); } // allow selection of the first point if (!this.getSourcePort()) { return super.getSelectionEntities().concat(_slice(this.points, 0, this.points.length - 1)); } // allow selection of the last point if (!this.getTargetPort()) { return super.getSelectionEntities().concat(_slice(this.points, 1, this.points.length)); } return super.getSelectionEntities().concat(this.points); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.points = _map(event.data.points || [], (point) => { var p = new PointModel({ link: this, position: new Point(point.x, point.y) }); p.deserialize({ ...event, data: point }); return p; }); //deserialize labels _forEach(event.data.labels || [], (label: any) => { let labelOb = (event.engine as DiagramEngine).getFactoryForLabel(label.type).generateModel({}); labelOb.deserialize({ ...event, data: label }); this.addLabel(labelOb); }); // these happen async, so we use the promises for these (they need to be done like this without the async keyword // because we need the deserailize method to finish for other methods while this happen if (event.data.target) { event.getModel(event.data.targetPort).then((model: PortModel) => { this.setTargetPort(model); }); } if (event.data.source) { event.getModel(event.data.sourcePort).then((model: PortModel) => { this.setSourcePort(model); }); } } getRenderedPath(): SVGPathElement[] { return this.renderedPaths; } setRenderedPaths(paths: SVGPathElement[]) { this.renderedPaths = paths; } serialize() { return { ...super.serialize(), source: this.sourcePort ? this.sourcePort.getParent().getID() : null, sourcePort: this.sourcePort ? this.sourcePort.getID() : null, target: this.targetPort ? this.targetPort.getParent().getID() : null, targetPort: this.targetPort ? this.targetPort.getID() : null, points: _map(this.points, (point) => { return point.serialize(); }), labels: _map(this.labels, (label) => { return label.serialize(); }) }; } doClone(lookupTable = {}, clone) { clone.setPoints( _map(this.getPoints(), (point: PointModel) => { return point.clone(lookupTable); }) ); if (this.sourcePort) { clone.setSourcePort(this.sourcePort.clone(lookupTable)); } if (this.targetPort) { clone.setTargetPort(this.targetPort.clone(lookupTable)); } } clearPort(port: PortModel) { if (this.sourcePort === port) { this.setSourcePort(null); } else if (this.targetPort === port) { this.setTargetPort(null); } } remove() { if (this.sourcePort) { this.sourcePort.removeLink(this); delete this.sourcePort; } if (this.targetPort) { this.targetPort.removeLink(this); delete this.targetPort; } super.remove(); } isLastPoint(point: PointModel) { var index = this.getPointIndex(point); return index === this.points.length - 1; } getPointIndex(point: PointModel) { return this.points.indexOf(point); } getPointModel(id: string): PointModel | null { for (var i = 0; i < this.points.length; i++) { if (this.points[i].getID() === id) { return this.points[i]; } } return null; } getPortForPoint(point: PointModel): PortModel { if (this.sourcePort !== null && this.getFirstPoint().getID() === point.getID()) { return this.sourcePort; } if (this.targetPort !== null && this.getLastPoint().getID() === point.getID()) { return this.targetPort; } return null; } getPointForPort(port: PortModel): PointModel { if (this.sourcePort !== null && this.sourcePort.getID() === port.getID()) { return this.getFirstPoint(); } if (this.targetPort !== null && this.targetPort.getID() === port.getID()) { return this.getLastPoint(); } return null; } getFirstPoint(): PointModel { return this.points[0]; } getLastPoint(): PointModel { return this.points[this.points.length - 1]; } setSourcePort(port: PortModel | null) { if (port !== null) { port.addLink(this); } if (this.sourcePort !== null) { this.sourcePort.removeLink(this); } this.sourcePort = port; this.fireEvent({ port }, 'sourcePortChanged'); if (port?.reportedPosition) { this.getPointForPort(port).setPosition(port.getCenter()); } } getSourcePort(): PortModel { return this.sourcePort; } getTargetPort(): PortModel { return this.targetPort; } setTargetPort(port: PortModel | null) { if (port !== null) { port.addLink(this); } if (this.targetPort !== null) { this.targetPort.removeLink(this); } this.targetPort = port; this.fireEvent({ port }, 'targetPortChanged'); if (port?.reportedPosition) { this.getPointForPort(port).setPosition(port.getCenter()); } } point(x: number, y: number, index: number = 1): PointModel { return this.addPoint(this.generatePoint(x, y), index); } addLabel(label: LabelModel) { label.setParent(this); this.labels.push(label); } getPoints(): PointModel[] { return this.points; } getLabels() { return this.labels; } setPoints(points: PointModel[]) { _forEach(points, (point) => { point.setParent(this); }); this.points = points; } removePoint(pointModel: PointModel) { if (this.isLastPoint(pointModel)) this.remove(); this.points.splice(this.getPointIndex(pointModel), 1); } removePointsBefore(pointModel: PointModel) { this.points.splice(0, this.getPointIndex(pointModel)); } removePointsAfter(pointModel: PointModel) { this.points.splice(this.getPointIndex(pointModel) + 1); } removeMiddlePoints() { if (this.points.length > 2) { this.points.splice(1, this.points.length - 2); } } addPoint

(pointModel: P, index = 1): P { pointModel.setParent(this); this.points.splice(index, 0, pointModel); return pointModel; } generatePoint(x: number = 0, y: number = 0): PointModel { return new PointModel({ link: this, position: new Point(x, y) }); } } ================================================ FILE: packages/react-diagrams-core/src/entities/link/LinkWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine } from '../../DiagramEngine'; import { LinkModel } from './LinkModel'; import { PointModel } from './PointModel'; import _map from 'lodash/map'; import { LabelWidget } from '../label/LabelWidget'; import { BaseEntityEvent, BasePositionModel, ListenerHandle, PeformanceWidget } from '@projectstorm/react-canvas-core'; import { PortModel } from '../port/PortModel'; export interface LinkProps { link: LinkModel; diagramEngine: DiagramEngine; } export interface LinkState { sourcePort: PortModel; targetPort: PortModel; } export class LinkWidget extends React.Component { sourceListener: ListenerHandle; targetListener: ListenerHandle; constructor(props) { super(props); this.state = { sourcePort: null, targetPort: null }; } componentWillUnmount(): void { if (this.sourceListener) { this.sourceListener.deregister(); } if (this.targetListener) { this.targetListener.deregister(); } } static getDerivedStateFromProps(nextProps: LinkProps, prevState: LinkState): LinkState { return { sourcePort: nextProps.link.getSourcePort(), targetPort: nextProps.link.getTargetPort() }; } installTarget() { this.targetListener && this.targetListener.deregister(); if (!this.props.link.getTargetPort()) return; this.targetListener = this.props.link.getTargetPort().registerListener({ reportInitialPosition: (event: BaseEntityEvent) => { this.forceUpdate(); } }); } installSource() { this.sourceListener && this.sourceListener.deregister(); if (!this.props.link.getSourcePort()) return; this.sourceListener = this.props.link.getSourcePort().registerListener({ reportInitialPosition: (event: BaseEntityEvent) => { this.forceUpdate(); } }); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot) { if (prevState.sourcePort !== this.state.sourcePort) { this.installSource(); } if (prevState.targetPort !== this.state.targetPort) { this.installTarget(); } } public static generateLinePath(firstPoint: PointModel, lastPoint: PointModel): string { return `M${firstPoint.getX()},${firstPoint.getY()} L ${lastPoint.getX()},${lastPoint.getY()}`; } componentDidMount(): void { if (this.props.link.getSourcePort()) { this.installSource(); } if (this.props.link.getTargetPort()) { this.installTarget(); } } render() { const { link } = this.props; // only draw the link when we have reported positions if (link.getSourcePort() && !link.getSourcePort().reportedPosition) { return null; } if (link.getTargetPort() && !link.getTargetPort().reportedPosition) { return null; } //generate links return ( {() => { return ( {this.props.diagramEngine.generateWidgetForLink(link)} {_map(this.props.link.getLabels(), (labelModel, index) => { return ( ); })} ); }} ); } } ================================================ FILE: packages/react-diagrams-core/src/entities/link/PointModel.ts ================================================ import { LinkModel } from './LinkModel'; import { BaseModelListener, BasePositionModel, BasePositionModelGenerics, BasePositionModelOptions } from '@projectstorm/react-canvas-core'; export interface PointModelOptions extends Omit { link: LinkModel; } export interface PointModelGenerics { PARENT: LinkModel; OPTIONS: PointModelOptions; LISTENER: BaseModelListener; } export class PointModel extends BasePositionModel< G & BasePositionModelGenerics > { constructor(options: G['OPTIONS']) { super({ ...options, type: 'point' }); this.parent = options.link; } isConnectedToPort(): boolean { return this.parent.getPortForPoint(this) !== null; } getLink(): LinkModel { return this.getParent(); } remove() { //clear references if (this.parent) { this.parent.removePoint(this); } super.remove(); } isLocked() { return super.isLocked() || this.getParent().isLocked(); } } ================================================ FILE: packages/react-diagrams-core/src/entities/link-layer/LinkLayerFactory.tsx ================================================ import * as React from 'react'; import { AbstractReactFactory, GenerateModelEvent, GenerateWidgetEvent } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '../../DiagramEngine'; import { LinkLayerModel } from './LinkLayerModel'; import { LinkLayerWidget } from './LinkLayerWidget'; import { JSX } from 'react'; export class LinkLayerFactory extends AbstractReactFactory { constructor() { super('diagram-links'); } generateModel(event: GenerateModelEvent): LinkLayerModel { return new LinkLayerModel(); } generateReactWidget(event: GenerateWidgetEvent): JSX.Element { return ; } } ================================================ FILE: packages/react-diagrams-core/src/entities/link-layer/LinkLayerModel.ts ================================================ import { LayerModel, LayerModelGenerics } from '@projectstorm/react-canvas-core'; import { LinkModel } from '../link/LinkModel'; import { DiagramEngine } from '../../DiagramEngine'; import { DiagramModel } from '../../models/DiagramModel'; export interface LinkLayerModelGenerics extends LayerModelGenerics { CHILDREN: LinkModel; ENGINE: DiagramEngine; } export class LinkLayerModel extends LayerModel { constructor() { super({ type: 'diagram-links', isSvg: true, transformed: true }); } addModel(model: G['CHILDREN']): void { if (!(model instanceof LinkModel)) { throw new Error('Can only add links to this layer'); } model.registerListener({ entityRemoved: () => { (this.getParent() as DiagramModel).removeLink(model); } }); super.addModel(model); } getLinks() { return this.getModels(); } getChildModelFactoryBank(engine: G['ENGINE']) { return engine.getLinkFactories(); } } ================================================ FILE: packages/react-diagrams-core/src/entities/link-layer/LinkLayerWidget.tsx ================================================ import * as React from 'react'; import styled from '@emotion/styled'; import _map from 'lodash/map'; import { LinkWidget } from '../link/LinkWidget'; import { LinkLayerModel } from './LinkLayerModel'; import { DiagramEngine } from '../../DiagramEngine'; export interface LinkLayerWidgetProps { layer: LinkLayerModel; engine: DiagramEngine; } namespace S { export const Container = styled.div``; } export class LinkLayerWidget extends React.Component { render() { return ( <> { //only perform these actions when we have a diagram _map(this.props.layer.getLinks(), (link) => { return ; }) } ); } } ================================================ FILE: packages/react-diagrams-core/src/entities/node/NodeModel.ts ================================================ import _forEach from 'lodash/forEach'; import _map from 'lodash/map'; import _values from 'lodash/values'; import { DiagramModel } from '../../models/DiagramModel'; import { PortModel } from '../port/PortModel'; import { LinkModel } from '../link/LinkModel'; import { Point, Rectangle } from '@projectstorm/geometry'; import { BaseEntityEvent, BaseModelListener, BasePositionModel, BasePositionModelGenerics, DeserializeEvent } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '../../DiagramEngine'; export interface NodeModelListener extends BaseModelListener { positionChanged?(event: BaseEntityEvent): void; } export interface NodeModelGenerics extends BasePositionModelGenerics { LISTENER: NodeModelListener; PARENT: DiagramModel; } export class NodeModel extends BasePositionModel { protected ports: { [s: string]: PortModel }; // calculated post rendering so routing can be done correctly width: number; height: number; constructor(options: G['OPTIONS']) { super(options); this.ports = {}; this.width = 0; this.height = 0; } getBoundingBox(): Rectangle { return Rectangle.fromPointAndSize(this.getPosition(), this.width, this.height); } setPosition(point: Point): void; setPosition(x: number, y: number): void; setPosition(x: number | Point, y?: number): void { const old = this.position; if (x instanceof Point) { super.setPosition(x); } else { super.setPosition(x, y); } //also update the port co-ordinates (for make glorious speed) _forEach(this.ports, (port) => { port.setPosition(port.getX() + this.position.x - old.x, port.getY() + this.position.y - old.y); }); } deserialize(event: DeserializeEvent) { super.deserialize(event); //deserialize ports _forEach(event.data.ports, (port: any) => { let portOb = (event.engine as DiagramEngine).getFactoryForPort(port.type).generateModel({}); portOb.deserialize({ ...event, data: port }); // the links need these event.registerModel(portOb); this.addPort(portOb); }); } serialize() { return { ...super.serialize(), ports: _map(this.ports, (port) => { return port.serialize(); }) }; } doClone(lookupTable = {}, clone) { // also clone the ports clone.ports = {}; _forEach(this.ports, (port) => { clone.addPort(port.clone(lookupTable)); }); } remove() { super.remove(); _forEach(this.ports, (port) => { _forEach(port.getLinks(), (link) => { link.remove(); }); }); } getPortFromID(id): PortModel | null { for (var i in this.ports) { if (this.ports[i].getID() === id) { return this.ports[i]; } } return null; } getLink(id: string): LinkModel { for (let portID in this.ports) { const links = this.ports[portID].getLinks(); if (links[id]) { return links[id]; } } } getPort(name: string): PortModel | null { return this.ports[name]; } getPorts(): { [s: string]: PortModel } { return this.ports; } removePort(port: PortModel) { // clear the port from the links for (let link of _values(port.getLinks())) { link.clearPort(port); } //clear the parent node reference if (this.ports[port.getName()]) { this.ports[port.getName()].setParent(null); delete this.ports[port.getName()]; } } addPort(port: PortModel): PortModel { port.setParent(this); this.ports[port.getName()] = port; return port; } updateDimensions({ width, height }: { width: number; height: number }) { this.width = width; this.height = height; } } ================================================ FILE: packages/react-diagrams-core/src/entities/node/NodeWidget.tsx ================================================ import * as React from 'react'; import _forEach from 'lodash/forEach'; import { DiagramEngine } from '../../DiagramEngine'; import { NodeModel } from './NodeModel'; import { BaseEntityEvent, BaseModel, ListenerHandle, PeformanceWidget } from '@projectstorm/react-canvas-core'; import styled from '@emotion/styled'; import ResizeObserver from 'resize-observer-polyfill'; export interface NodeProps { node: NodeModel; children?: any; diagramEngine: DiagramEngine; } namespace S { export const Node = styled.div` position: absolute; -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Chrome/Safari/Opera */ user-select: none; cursor: move; pointer-events: all; `; } export class NodeWidget extends React.Component { ob: any; ref: React.RefObject; listener: ListenerHandle; constructor(props: NodeProps) { super(props); this.ref = React.createRef(); } componentWillUnmount(): void { this.ob.disconnect(); this.ob = null; this.listener?.deregister(); this.listener = null; } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { if (this.listener && this.props.node !== prevProps.node) { this.listener.deregister(); this.installSelectionListener(); } } installSelectionListener() { this.listener = this.props.node.registerListener({ selectionChanged: (event: BaseEntityEvent & { isSelected: boolean }) => { this.forceUpdate(); } }); } updateSize(width: number, height: number) { this.props.node.updateDimensions({ width, height }); //now mark the links as dirty try { _forEach(this.props.node.getPorts(), (port) => { port.updateCoords(this.props.diagramEngine.getPortCoords(port)); }); } catch (ex) {} } componentDidMount(): void { // @ts-ignore this.ob = new ResizeObserver((entities) => { const bounds = entities[0].contentRect; this.updateSize(bounds.width, bounds.height); }); const b = this.ref.current.getBoundingClientRect(); this.updateSize(b.width, b.height); this.ob.observe(this.ref.current); this.installSelectionListener(); } render() { return ( {() => { return ( {this.props.diagramEngine.generateWidgetForNode(this.props.node)} ); }} ); } } ================================================ FILE: packages/react-diagrams-core/src/entities/node-layer/NodeLayerFactory.tsx ================================================ import * as React from 'react'; import { AbstractReactFactory, GenerateModelEvent, GenerateWidgetEvent } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '../../DiagramEngine'; import { NodeLayerModel } from './NodeLayerModel'; import { NodeLayerWidget } from './NodeLayerWidget'; import { JSX } from 'react'; export class NodeLayerFactory extends AbstractReactFactory { constructor() { super('diagram-nodes'); } generateModel(event: GenerateModelEvent): NodeLayerModel { return new NodeLayerModel(); } generateReactWidget(event: GenerateWidgetEvent): JSX.Element { return ; } } ================================================ FILE: packages/react-diagrams-core/src/entities/node-layer/NodeLayerModel.ts ================================================ import { LayerModel, LayerModelGenerics } from '@projectstorm/react-canvas-core'; import { NodeModel } from '../node/NodeModel'; import { DiagramEngine } from '../../DiagramEngine'; import { DiagramModel } from '../../models/DiagramModel'; export interface NodeLayerModelGenerics extends LayerModelGenerics { CHILDREN: NodeModel; ENGINE: DiagramEngine; } export class NodeLayerModel extends LayerModel { constructor() { super({ type: 'diagram-nodes', isSvg: false, transformed: true }); } addModel(model: G['CHILDREN']): void { if (!(model instanceof NodeModel)) { throw new Error('Can only add nodes to this layer'); } model.registerListener({ entityRemoved: () => { (this.getParent() as DiagramModel).removeNode(model); } }); super.addModel(model); } getChildModelFactoryBank(engine: G['ENGINE']) { return engine.getNodeFactories(); } getNodes() { return this.getModels(); } } ================================================ FILE: packages/react-diagrams-core/src/entities/node-layer/NodeLayerWidget.tsx ================================================ import * as React from 'react'; import _map from 'lodash/map'; import { NodeModel } from '../node/NodeModel'; import { NodeWidget } from '../node/NodeWidget'; import { NodeLayerModel } from './NodeLayerModel'; import { DiagramEngine } from '../../DiagramEngine'; export interface NodeLayerWidgetProps { layer: NodeLayerModel; engine: DiagramEngine; } export class NodeLayerWidget extends React.Component { render() { return ( <> {_map(this.props.layer.getNodes(), (node: NodeModel) => { return ; })} ); } } ================================================ FILE: packages/react-diagrams-core/src/entities/port/PortModel.ts ================================================ import { NodeModel } from '../node/NodeModel'; import { LinkModel } from '../link/LinkModel'; import _forEach from 'lodash/forEach'; import _isFinite from 'lodash/isFinite'; import _map from 'lodash/map'; import _size from 'lodash/size'; import _values from 'lodash/values'; import { Point, Rectangle } from '@projectstorm/geometry'; import { BaseEntityEvent, BaseModelOptions, BasePositionModel, BasePositionModelGenerics, BasePositionModelListener, DeserializeEvent } from '@projectstorm/react-canvas-core'; export enum PortModelAlignment { TOP = 'top', LEFT = 'left', BOTTOM = 'bottom', RIGHT = 'right' } export interface PortModelListener extends BasePositionModelListener { /** * fires when it first receives positional information */ reportInitialPosition?: (event: BaseEntityEvent) => void; } export interface PortModelOptions extends BaseModelOptions { alignment?: PortModelAlignment; maximumLinks?: number; name: string; } export interface PortModelGenerics extends BasePositionModelGenerics { OPTIONS: PortModelOptions; PARENT: NodeModel; LISTENER: PortModelListener; } export class PortModel extends BasePositionModel { links: { [id: string]: LinkModel }; // calculated post rendering so routing can be done correctly width: number; height: number; reportedPosition: boolean; constructor(options: G['OPTIONS']) { super(options); this.links = {}; this.reportedPosition = false; } deserialize(event: DeserializeEvent) { super.deserialize(event); this.reportedPosition = false; this.options.name = event.data.name; this.options.alignment = event.data.alignment; } serialize() { return { ...super.serialize(), name: this.options.name, alignment: this.options.alignment, parentNode: this.parent.getID(), links: _map(this.links, (link) => { return link.getID(); }) }; } setPosition(point: Point); setPosition(x: number, y: number); setPosition(x, y?) { let old = this.position; super.setPosition(x, y); _forEach(this.getLinks(), (link) => { let point = link.getPointForPort(this); point.setPosition(point.getX() + x - old.x, point.getY() + y - old.y); }); } doClone(lookupTable = {}, clone: PortModel) { clone.links = {}; clone.parent = this.getParent().clone(lookupTable); } getNode(): NodeModel { return this.getParent(); } getName(): string { return this.options.name; } getMaximumLinks(): number { return this.options.maximumLinks; } setMaximumLinks(maximumLinks: number) { this.options.maximumLinks = maximumLinks; } removeLink(link: LinkModel) { delete this.links[link.getID()]; } addLink(link: LinkModel) { this.links[link.getID()] = link; } getLinks(): { [id: string]: LinkModel } { return this.links; } public createLinkModel(): LinkModel | null { if (_isFinite(this.options.maximumLinks)) { var numberOfLinks: number = _size(this.links); if (this.options.maximumLinks === 1 && numberOfLinks >= 1) { return _values(this.links)[0]; } else if (numberOfLinks >= this.options.maximumLinks) { return null; } } return null; } reportPosition() { _forEach(this.getLinks(), (link) => { link.getPointForPort(this).setPosition(this.getCenter()); }); this.fireEvent( { entity: this }, 'reportInitialPosition' ); } getCenter(): Point { return new Point(this.getX() + this.width / 2, this.getY() + this.height / 2); } getBoundingBox(): Rectangle { return Rectangle.fromPointAndSize(this.position, this.width, this.height); } updateCoords(coords: Rectangle) { this.width = coords.getWidth(); this.height = coords.getHeight(); this.setPosition(coords.getTopLeft()); this.reportedPosition = true; this.reportPosition(); } canLinkToPort(port: PortModel): boolean { return true; } isLocked() { return super.isLocked() || this.getParent().isLocked(); } } ================================================ FILE: packages/react-diagrams-core/src/entities/port/PortWidget.tsx ================================================ import * as React from 'react'; import _keys from 'lodash/keys'; import { PortModel } from './PortModel'; import { DiagramEngine } from '../../DiagramEngine'; import { ListenerHandle, Toolkit } from '@projectstorm/react-canvas-core'; export interface PortProps { port: PortModel; engine: DiagramEngine; className?; style?; } export class PortWidget extends React.Component> { ref: React.RefObject; engineListenerHandle: ListenerHandle; constructor(props: PortProps) { super(props); this.ref = React.createRef(); } report() { this.props.port.updateCoords(this.props.engine.getPortCoords(this.props.port, this.ref.current)); } componentWillUnmount(): void { this.engineListenerHandle && this.engineListenerHandle.deregister(); } componentDidUpdate(prevProps: Readonly, prevState, snapshot?: any): void { if (!this.props.port.reportedPosition) { this.report(); } } componentDidMount(): void { this.engineListenerHandle = this.props.engine.registerListener({ canvasReady: () => { this.report(); } }); if (this.props.engine.getCanvas()) { this.report(); } } getExtraProps() { if (Toolkit.TESTING) { const links = _keys(this.props.port.getNode().getPort(this.props.port.getName()).links).join(','); return { 'data-links': links }; } return {}; } render() { return (

{this.props.children}
); } } ================================================ FILE: packages/react-diagrams-core/src/index.ts ================================================ export * from './models/DiagramModel'; export * from './entities/label/LabelModel'; export * from './entities/link/LinkModel'; export * from './entities/link/PointModel'; export * from './entities/link/LinkWidget'; export * from './entities/link-layer/LinkLayerModel'; export * from './entities/link-layer/LinkLayerWidget'; export * from './entities/link-layer/LinkLayerFactory'; export * from './entities/node-layer/NodeLayerModel'; export * from './entities/node-layer/NodeLayerWidget'; export * from './entities/node-layer/NodeLayerFactory'; export * from './entities/node/NodeModel'; export * from './entities/node/NodeWidget'; export * from './entities/port/PortModel'; export * from './entities/port/PortWidget'; export * from './states/DefaultDiagramState'; export * from './states/DragDiagramItemsState'; export * from './states/DragNewLinkState'; export * from './DiagramEngine'; ================================================ FILE: packages/react-diagrams-core/src/models/DiagramModel.ts ================================================ import _filter from 'lodash/filter'; import _flatMap from 'lodash/flatMap'; import _forEach from 'lodash/forEach'; import _some from 'lodash/some'; import _values from 'lodash/values'; import { LinkModel } from '../entities/link/LinkModel'; import { NodeModel } from '../entities/node/NodeModel'; import { BaseEntityEvent, BaseEntityListener, BaseModel, CanvasModel, CanvasModelGenerics, LayerModel, DeserializeEvent } from '@projectstorm/react-canvas-core'; import { NodeLayerModel } from '../entities/node-layer/NodeLayerModel'; import { LinkLayerModel } from '../entities/link-layer/LinkLayerModel'; export interface DiagramListener extends BaseEntityListener { nodesUpdated?(event: BaseEntityEvent & { node: NodeModel; isCreated: boolean }): void; linksUpdated?(event: BaseEntityEvent & { link: LinkModel; isCreated: boolean }): void; } export interface DiagramModelGenerics extends CanvasModelGenerics { LISTENER: DiagramListener; } export class DiagramModel extends CanvasModel { protected activeNodeLayer: NodeLayerModel; protected activeLinkLayer: LinkLayerModel; constructor(options: G['OPTIONS'] = {}) { super(options); this.addLayer(new LinkLayerModel()); this.addLayer(new NodeLayerModel()); } deserialize(event: DeserializeEvent) { this.layers = []; super.deserialize(event); } addLayer(layer: LayerModel): void { super.addLayer(layer); if (layer instanceof NodeLayerModel) { this.activeNodeLayer = layer; } if (layer instanceof LinkLayerModel) { this.activeLinkLayer = layer; } } getLinkLayers(): LinkLayerModel[] { return _filter(this.layers, (layer) => { return layer instanceof LinkLayerModel; }) as LinkLayerModel[]; } getNodeLayers(): NodeLayerModel[] { return _filter(this.layers, (layer) => { return layer instanceof NodeLayerModel; }) as NodeLayerModel[]; } getActiveNodeLayer(): NodeLayerModel { if (!this.activeNodeLayer) { const layers = this.getNodeLayers(); if (layers.length === 0) { this.addLayer(new NodeLayerModel()); } else { this.activeNodeLayer = layers[0]; } } return this.activeNodeLayer; } getActiveLinkLayer(): LinkLayerModel { if (!this.activeLinkLayer) { const layers = this.getLinkLayers(); if (layers.length === 0) { this.addLayer(new LinkLayerModel()); } else { this.activeLinkLayer = layers[0]; } } return this.activeLinkLayer; } getNode(node: string): NodeModel { for (const layer of this.getNodeLayers()) { const model = layer.getModel(node); if (model) { return model; } } } getLink(link: string): LinkModel { for (const layer of this.getLinkLayers()) { const model = layer.getModel(link); if (model) { return model; } } } addAll(...models: BaseModel[]): BaseModel[] { _forEach(models, (model) => { if (model instanceof LinkModel) { this.addLink(model); } else if (model instanceof NodeModel) { this.addNode(model); } }); return models; } addLink(link: LinkModel): LinkModel { this.getActiveLinkLayer().addModel(link); this.fireEvent( { link, isCreated: true }, 'linksUpdated' ); return link; } addNode(node: NodeModel): NodeModel { this.getActiveNodeLayer().addModel(node); this.fireEvent({ node, isCreated: true }, 'nodesUpdated'); return node; } removeLink(link: LinkModel) { const removed = _some(this.getLinkLayers(), (layer) => { return layer.removeModel(link); }); if (removed) { this.fireEvent({ link, isCreated: false }, 'linksUpdated'); } } removeNode(node: NodeModel) { const removed = _some(this.getNodeLayers(), (layer) => { return layer.removeModel(node); }); if (removed) { this.fireEvent({ node, isCreated: false }, 'nodesUpdated'); } } getLinks(): LinkModel[] { return _flatMap(this.getLinkLayers(), (layer) => { return _values(layer.getModels()); }); } getNodes(): NodeModel[] { return _flatMap(this.getNodeLayers(), (layer) => { return _values(layer.getModels()); }); } } ================================================ FILE: packages/react-diagrams-core/src/states/DefaultDiagramState.ts ================================================ import { MouseEvent, TouchEvent } from 'react'; import { SelectingState, State, Action, InputType, ActionEvent, DragCanvasState } from '@projectstorm/react-canvas-core'; import { PortModel } from '../entities/port/PortModel'; import { DragNewLinkState } from './DragNewLinkState'; import { DiagramEngine } from '../DiagramEngine'; import { DragDiagramItemsState } from './DragDiagramItemsState'; export class DefaultDiagramState extends State { dragCanvas: DragCanvasState; dragNewLink: DragNewLinkState; dragItems: DragDiagramItemsState; constructor() { super({ name: 'default-diagrams' }); this.childStates = [new SelectingState()]; this.dragCanvas = new DragCanvasState(); this.dragNewLink = new DragNewLinkState(); this.dragItems = new DragDiagramItemsState(); // determine what was clicked on this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { const element = this.engine.getActionEventBus().getModelForEvent(event); // the canvas was clicked on, transition to the dragging canvas state if (!element) { this.transitionWithEvent(this.dragCanvas, event); } // initiate dragging a new link else if (element instanceof PortModel) { this.transitionWithEvent(this.dragNewLink, event); } // move the items (and potentially link points) else { this.transitionWithEvent(this.dragItems, event); } } }) ); // touch drags the canvas this.registerAction( new Action({ type: InputType.TOUCH_START, fire: (event: ActionEvent) => { this.transitionWithEvent(this.dragCanvas, event); } }) ); } } ================================================ FILE: packages/react-diagrams-core/src/states/DragDiagramItemsState.ts ================================================ import { Action, ActionEvent, InputType, MoveItemsState } from '@projectstorm/react-canvas-core'; import _forEach from 'lodash/forEach'; import { PointModel } from '../entities/link/PointModel'; import { DiagramEngine } from '../DiagramEngine'; import { PortModel } from '../entities/port/PortModel'; import { MouseEvent } from 'react'; import { LinkModel } from '../entities/link/LinkModel'; export class DragDiagramItemsState extends MoveItemsState { constructor() { super(); this.registerAction( new Action({ type: InputType.MOUSE_UP, fire: (event: ActionEvent) => { const item = this.engine.getMouseElement(event.event); if (item instanceof PortModel) { _forEach(this.initialPositions, (position) => { if (position.item instanceof PointModel) { const link = position.item.getParent() as LinkModel; // only care about the last links if (link.getLastPoint() !== position.item) { return; } if (link.getSourcePort().canLinkToPort(item)) { link.setTargetPort(item); item.reportPosition(); this.engine.repaintCanvas(); } } }); } } }) ); } } ================================================ FILE: packages/react-diagrams-core/src/states/DragNewLinkState.ts ================================================ import { AbstractDisplacementState, AbstractDisplacementStateEvent, Action, ActionEvent, InputType } from '@projectstorm/react-canvas-core'; import { PortModel } from '../entities/port/PortModel'; import { MouseEvent } from 'react'; import { LinkModel } from '../entities/link/LinkModel'; import { DiagramEngine } from '../DiagramEngine'; export interface DragNewLinkStateOptions { /** * If enabled, the links will stay on the canvas if they dont connect to a port * when dragging finishes */ allowLooseLinks?: boolean; /** * If enabled, then a link can still be drawn from the port even if it is locked */ allowLinksFromLockedPorts?: boolean; } export class DragNewLinkState extends AbstractDisplacementState { port: PortModel; link: LinkModel; config: DragNewLinkStateOptions; constructor(options: DragNewLinkStateOptions = {}) { super({ name: 'drag-new-link' }); this.config = { allowLooseLinks: true, allowLinksFromLockedPorts: false, ...options }; this.registerAction( new Action({ type: InputType.MOUSE_DOWN, fire: (event: ActionEvent) => { this.port = this.engine.getMouseElement(event.event) as PortModel; if (!this.config.allowLinksFromLockedPorts && this.port.isLocked()) { this.eject(); return; } this.link = this.port.createLinkModel(); // if no link is given, just eject the state if (!this.link) { this.eject(); return; } this.link.setSelected(true); this.link.setSourcePort(this.port); this.engine.getModel().addLink(this.link); this.port.reportPosition(); } }) ); this.registerAction( new Action({ type: InputType.MOUSE_UP, fire: (event: ActionEvent) => { const model = this.engine.getMouseElement(event.event); // check to see if we connected to a new port if (model instanceof PortModel) { if (this.port.canLinkToPort(model)) { this.link.setTargetPort(model); model.reportPosition(); this.engine.repaintCanvas(); return; } else { this.link.remove(); this.engine.repaintCanvas(); return; } } if (!this.config.allowLooseLinks) { this.link.remove(); this.engine.repaintCanvas(); } } }) ); } /** * Calculates the link's far-end point position on mouse move. * In order to be as precise as possible the mouse initialXRelative & initialYRelative are taken into account as well * as the possible engine offset */ fireMouseMoved(event: AbstractDisplacementStateEvent): any { const portPos = this.port.getPosition(); const zoomLevelPercentage = this.engine.getModel().getZoomLevel() / 100; const engineOffsetX = this.engine.getModel().getOffsetX() / zoomLevelPercentage; const engineOffsetY = this.engine.getModel().getOffsetY() / zoomLevelPercentage; const initialXRelative = this.initialXRelative / zoomLevelPercentage; const initialYRelative = this.initialYRelative / zoomLevelPercentage; const linkNextX = portPos.x - engineOffsetX + (initialXRelative - portPos.x) + event.virtualDisplacementX; const linkNextY = portPos.y - engineOffsetY + (initialYRelative - portPos.y) + event.virtualDisplacementY; this.link.getLastPoint().setPosition(linkNextX, linkNextY); this.engine.repaintCanvas(); } } ================================================ FILE: packages/react-diagrams-core/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "outDir": "dist", "rootDir": "src", "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ { "path": "../react-canvas-core" }, { "path": "../geometry" } ] } ================================================ FILE: packages/react-diagrams-core/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: 'projectstorm/react-canvas-core' } }; ================================================ FILE: packages/react-diagrams-defaults/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/react-diagrams-defaults/CHANGELOG.md ================================================ # @projectstorm/react-diagrams-defaults ## 7.1.3 ### Patch Changes - 20766f5: fix default link widget with react strict mode - 80285fe: refactor: update lodash imports to use individual functions - Updated dependencies [09ed60f] - Updated dependencies [80285fe] - @projectstorm/react-canvas-core@7.0.3 - @projectstorm/react-diagrams-core@7.0.3 - @projectstorm/geometry@7.0.3 ## 7.1.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - Updated dependencies [66c687a] - @projectstorm/react-diagrams-core@7.0.2 - @projectstorm/react-canvas-core@7.0.2 - @projectstorm/geometry@7.0.2 ## 7.1.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/geometry@7.0.1 - @projectstorm/react-canvas-core@7.0.1 - @projectstorm/react-diagrams-core@7.0.1 ## 7.1.0 ### Minor Changes - e0d21f1: - [feature] new ability to refresh links in auto distribute system [PR 756](https://github.com/projectstorm/react-diagrams/pull/756) - [fix] Default link now uses the correct method for creating a point allowing this to be overridden [PR 939](https://github.com/projectstorm/react-diagrams/pull/939) Big thanks to @ToTheHit and @h0111in for your help on these, even though its very delayed on my part :) ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-diagrams-core@7.0.0 - @projectstorm/react-canvas-core@7.0.0 - @projectstorm/geometry@7.0.0 ================================================ FILE: packages/react-diagrams-defaults/README.md ================================================ # Project STORM > React Diagrams > Defaults This workspace houses the default models ================================================ FILE: packages/react-diagrams-defaults/package.json ================================================ { "name": "@projectstorm/react-diagrams-defaults", "version": "7.1.3", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.*", "@projectstorm/geometry": "workspace:*", "@projectstorm/react-canvas-core": "workspace:*", "@projectstorm/react-diagrams-core": "workspace:*", "lodash": "^4.17.21", "react": "^19.0.0" }, "devDependencies": { "@types/lodash": "^4.14.200", "@types/react": "^19.0.12" } } ================================================ FILE: packages/react-diagrams-defaults/src/index.ts ================================================ export * from './label/DefaultLabelFactory'; export * from './label/DefaultLabelModel'; export * from './label/DefaultLabelWidget'; export * from './link/DefaultLinkFactory'; export * from './link/DefaultLinkModel'; export * from './link/DefaultLinkWidget'; export * from './link/DefaultLinkSegmentWidget'; export * from './link/DefaultLinkPointWidget'; export * from './node/DefaultNodeFactory'; export * from './node/DefaultNodeModel'; export * from './node/DefaultNodeWidget'; export * from './port/DefaultPortFactory'; export * from './port/DefaultPortLabelWidget'; export * from './port/DefaultPortModel'; ================================================ FILE: packages/react-diagrams-defaults/src/label/DefaultLabelFactory.tsx ================================================ import * as React from 'react'; import { DefaultLabelModel } from './DefaultLabelModel'; import { DefaultLabelWidget } from './DefaultLabelWidget'; import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { JSX } from 'react'; /** * @author Dylan Vorster */ export class DefaultLabelFactory extends AbstractReactFactory { constructor() { super('default'); } generateReactWidget(event): JSX.Element { return ; } generateModel(event): DefaultLabelModel { return new DefaultLabelModel(); } } ================================================ FILE: packages/react-diagrams-defaults/src/label/DefaultLabelModel.tsx ================================================ import { LabelModel, LabelModelGenerics, LabelModelOptions } from '@projectstorm/react-diagrams-core'; import { DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface DefaultLabelModelOptions extends LabelModelOptions { label?: string; } export interface DefaultLabelModelGenerics extends LabelModelGenerics { OPTIONS: DefaultLabelModelOptions; } export class DefaultLabelModel extends LabelModel { constructor(options: DefaultLabelModelOptions = {}) { super({ offsetY: options.offsetY == null ? -23 : options.offsetY, type: 'default', ...options }); } setLabel(label: string) { this.options.label = label; } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.label = event.data.label; } serialize() { return { ...super.serialize(), label: this.options.label }; } } ================================================ FILE: packages/react-diagrams-defaults/src/label/DefaultLabelWidget.tsx ================================================ import * as React from 'react'; import { DefaultLabelModel } from './DefaultLabelModel'; import styled from '@emotion/styled'; export interface DefaultLabelWidgetProps { model: DefaultLabelModel; } namespace S { export const Label = styled.div` background: rgba(0, 0, 0, 0.8); border-radius: 5px; color: white; font-size: 12px; padding: 4px 8px; font-family: sans-serif; user-select: none; `; } export class DefaultLabelWidget extends React.Component { render() { return {this.props.model.getOptions().label}; } } ================================================ FILE: packages/react-diagrams-defaults/src/link/DefaultLinkFactory.tsx ================================================ import * as React from 'react'; import { DefaultLinkModel } from './DefaultLinkModel'; import { DefaultLinkWidget } from './DefaultLinkWidget'; import styled from '@emotion/styled'; import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { css, keyframes } from '@emotion/react'; import { JSX } from 'react'; namespace S { export const Keyframes = keyframes` from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; } `; const selected = css` stroke-dasharray: 10, 2; animation: ${Keyframes} 1s linear infinite; `; export const Path = styled.path<{ selected: boolean }>` ${(p) => p.selected && selected}; fill: none; pointer-events: auto; `; } export class DefaultLinkFactory extends AbstractReactFactory< Link, DiagramEngine > { constructor(type = 'default') { super(type); } generateReactWidget(event): JSX.Element { return ; } generateModel(event): Link { return new DefaultLinkModel() as Link; } generateLinkSegment(model: Link, selected: boolean, path: string) { return ( ); } } ================================================ FILE: packages/react-diagrams-defaults/src/link/DefaultLinkModel.ts ================================================ import { DiagramEngine, LabelModel, LinkModel, LinkModelGenerics, LinkModelListener, PortModel, PortModelAlignment } from '@projectstorm/react-diagrams-core'; import { DefaultLabelModel } from '../label/DefaultLabelModel'; import { BezierCurve } from '@projectstorm/geometry'; import { BaseEntityEvent, BaseModelOptions, DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface DefaultLinkModelListener extends LinkModelListener { colorChanged?(event: BaseEntityEvent & { color: null | string }): void; widthChanged?(event: BaseEntityEvent & { width: 0 | number }): void; } export interface DefaultLinkModelOptions extends BaseModelOptions { width?: number; color?: string; selectedColor?: string; curvyness?: number; type?: string; testName?: string; } export interface DefaultLinkModelGenerics extends LinkModelGenerics { LISTENER: DefaultLinkModelListener; OPTIONS: DefaultLinkModelOptions; } export class DefaultLinkModel extends LinkModel { constructor(options: DefaultLinkModelOptions = {}) { super({ type: 'default', width: options.width || 3, color: options.color || 'gray', selectedColor: options.selectedColor || 'rgb(0,192,255)', curvyness: 50, ...options }); } calculateControlOffset(port: PortModel): [number, number] { if (port.getOptions().alignment === PortModelAlignment.RIGHT) { return [this.options.curvyness, 0]; } else if (port.getOptions().alignment === PortModelAlignment.LEFT) { return [-this.options.curvyness, 0]; } else if (port.getOptions().alignment === PortModelAlignment.TOP) { return [0, -this.options.curvyness]; } return [0, this.options.curvyness]; } getSVGPath(): string { if (this.points.length == 2) { const curve = new BezierCurve(); curve.setSource(this.getFirstPoint().getPosition()); curve.setTarget(this.getLastPoint().getPosition()); curve.setSourceControl(this.getFirstPoint().getPosition().clone()); curve.setTargetControl(this.getLastPoint().getPosition().clone()); if (this.sourcePort) { curve.getSourceControl().translate(...this.calculateControlOffset(this.getSourcePort())); } if (this.targetPort) { curve.getTargetControl().translate(...this.calculateControlOffset(this.getTargetPort())); } return curve.getSVGCurve(); } } serialize() { return { ...super.serialize(), width: this.options.width, color: this.options.color, curvyness: this.options.curvyness, selectedColor: this.options.selectedColor }; } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.color = event.data.color; this.options.width = event.data.width; this.options.curvyness = event.data.curvyness; this.options.selectedColor = event.data.selectedColor; } addLabel(label: LabelModel | string) { if (label instanceof LabelModel) { return super.addLabel(label); } let labelOb = new DefaultLabelModel(); labelOb.setLabel(label); return super.addLabel(labelOb); } setWidth(width: number) { this.options.width = width; this.fireEvent({ width }, 'widthChanged'); } setColor(color: string) { this.options.color = color; this.fireEvent({ color }, 'colorChanged'); } } ================================================ FILE: packages/react-diagrams-defaults/src/link/DefaultLinkPointWidget.tsx ================================================ import * as React from 'react'; import { PointModel } from '@projectstorm/react-diagrams-core'; import styled from '@emotion/styled'; export interface DefaultLinkPointWidgetProps { point: PointModel; color?: string; colorSelected: string; } export interface DefaultLinkPointWidgetState { selected: boolean; } namespace S { export const PointTop = styled.circle` pointer-events: all; `; } export class DefaultLinkPointWidget extends React.Component { constructor(props) { super(props); this.state = { selected: false }; } render() { const { point } = this.props; return ( { this.setState({ selected: false }); }} onMouseEnter={() => { this.setState({ selected: true }); }} data-id={point.getID()} data-linkid={point.getLink().getID()} cx={point.getPosition().x} cy={point.getPosition().y} r={15} opacity={0.0} /> ); } } ================================================ FILE: packages/react-diagrams-defaults/src/link/DefaultLinkSegmentWidget.tsx ================================================ import * as React from 'react'; import { DefaultLinkFactory } from './DefaultLinkFactory'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { DefaultLinkModel } from './DefaultLinkModel'; export interface DefaultLinkSegmentWidgetProps { path: string; link: DefaultLinkModel; selected: boolean; forwardRef: React.RefObject; factory: DefaultLinkFactory; diagramEngine: DiagramEngine; onSelection: (selected: boolean) => any; extras: object; } export class DefaultLinkSegmentWidget extends React.Component { render() { const Bottom = React.cloneElement( this.props.factory.generateLinkSegment( this.props.link, this.props.selected || this.props.link.isSelected(), this.props.path ), { ref: this.props.forwardRef } ); const Top = React.cloneElement(Bottom, { strokeLinecap: 'round', onMouseLeave: () => { this.props.onSelection(false); }, onMouseEnter: () => { this.props.onSelection(true); }, ...this.props.extras, ref: null, 'data-linkid': this.props.link.getID(), strokeOpacity: this.props.selected ? 0.1 : 0, strokeWidth: 20, fill: 'none', onContextMenu: () => { if (!this.props.link.isLocked()) { event.preventDefault(); this.props.link.remove(); } } }); return ( {Bottom} {Top} ); } } ================================================ FILE: packages/react-diagrams-defaults/src/link/DefaultLinkWidget.tsx ================================================ import { DiagramEngine, LinkWidget, PointModel } from '@projectstorm/react-diagrams-core'; import * as React from 'react'; import { JSX, MouseEvent, useEffect, useRef } from 'react'; import { DefaultLinkModel } from './DefaultLinkModel'; import { DefaultLinkPointWidget } from './DefaultLinkPointWidget'; import { DefaultLinkSegmentWidget } from './DefaultLinkSegmentWidget'; export interface DefaultLinkProps { link: DefaultLinkModel; diagramEngine: DiagramEngine; pointAdded?: (point: PointModel, event: MouseEvent) => any; renderPoints?: boolean; selected?: (event: MouseEvent) => any; } export const DefaultLinkWidget: React.FC = (props) => { const [selected, setSelected] = React.useState(false); const refPaths = useRef[]>([]); const renderPoints = () => { return props.renderPoints ?? true; }; useEffect(() => { props.link.setRenderedPaths(refPaths.current.map((ref) => ref.current).filter(Boolean) as SVGPathElement[]); return () => { props.link.setRenderedPaths([]); }; }, [props.link]); const generateRef = () => { const ref = React.createRef(); refPaths.current.push(ref); return ref; }; const addPointToLink = (event: MouseEvent, index: number) => { if ( !event.shiftKey && !props.link.isLocked() && props.link.getPoints().length - 1 <= props.diagramEngine.getMaxNumberPointsPerLink() ) { const position = props.diagramEngine.getRelativeMousePoint(event); const point = props.link.point(position.x, position.y, index); event.persist(); event.stopPropagation(); props.diagramEngine.getActionEventBus().fireAction({ event, model: point }); } }; const generatePoint = (point: PointModel): JSX.Element => { return ( ); }; const generateLink = (path: string, extraProps: any, id: string | number): JSX.Element => { return ( ); }; const points = props.link.getPoints(); const paths = []; refPaths.current = []; // Reset the refPaths for the current render if (points.length === 2) { paths.push( generateLink( props.link.getSVGPath(), { onMouseDown: (event: MouseEvent) => { props.selected?.(event); addPointToLink(event, 1); } }, '0' ) ); if (props.link.getTargetPort() == null) { paths.push(generatePoint(points[1])); } } else { for (let j = 0; j < points.length - 1; j++) { paths.push( generateLink( LinkWidget.generateLinePath(points[j], points[j + 1]), { 'data-linkid': props.link.getID(), 'data-point': j, onMouseDown: (event: MouseEvent) => { props.selected?.(event); addPointToLink(event, j + 1); } }, j ) ); } if (renderPoints()) { for (let i = 1; i < points.length - 1; i++) { paths.push(generatePoint(points[i])); } if (props.link.getTargetPort() == null) { paths.push(generatePoint(points[points.length - 1])); } } } return {paths}; }; ================================================ FILE: packages/react-diagrams-defaults/src/node/DefaultNodeFactory.tsx ================================================ import * as React from 'react'; import { DefaultNodeModel } from './DefaultNodeModel'; import { DefaultNodeWidget } from './DefaultNodeWidget'; import { AbstractReactFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { JSX } from 'react'; export class DefaultNodeFactory extends AbstractReactFactory { constructor() { super('default'); } generateReactWidget(event): JSX.Element { return ; } generateModel(event): DefaultNodeModel { return new DefaultNodeModel(); } } ================================================ FILE: packages/react-diagrams-defaults/src/node/DefaultNodeModel.ts ================================================ import _map from 'lodash/map'; import { NodeModel, NodeModelGenerics, PortModelAlignment } from '@projectstorm/react-diagrams-core'; import { DefaultPortModel } from '../port/DefaultPortModel'; import { BasePositionModelOptions, DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface DefaultNodeModelOptions extends BasePositionModelOptions { name?: string; color?: string; } export interface DefaultNodeModelGenerics extends NodeModelGenerics { OPTIONS: DefaultNodeModelOptions; } export class DefaultNodeModel extends NodeModel { protected portsIn: DefaultPortModel[]; protected portsOut: DefaultPortModel[]; constructor(name: string, color: string); constructor(options?: DefaultNodeModelOptions); constructor(options: any = {}, color?: string) { if (typeof options === 'string') { options = { name: options, color: color }; } super({ type: 'default', name: 'Untitled', color: 'rgb(0,192,255)', ...options }); this.portsOut = []; this.portsIn = []; } doClone(lookupTable: {}, clone: any): void { clone.portsIn = []; clone.portsOut = []; super.doClone(lookupTable, clone); } removePort(port: DefaultPortModel): void { super.removePort(port); if (port.getOptions().in) { this.portsIn.splice(this.portsIn.indexOf(port), 1); } else { this.portsOut.splice(this.portsOut.indexOf(port), 1); } } addPort(port: T): T { super.addPort(port); if (port.getOptions().in) { if (this.portsIn.indexOf(port) === -1) { this.portsIn.push(port); } } else { if (this.portsOut.indexOf(port) === -1) { this.portsOut.push(port); } } return port; } addInPort(label: string, after = true): DefaultPortModel { const p = new DefaultPortModel({ in: true, name: label, label: label, alignment: PortModelAlignment.LEFT }); if (!after) { this.portsIn.splice(0, 0, p); } return this.addPort(p); } addOutPort(label: string, after = true): DefaultPortModel { const p = new DefaultPortModel({ in: false, name: label, label: label, alignment: PortModelAlignment.RIGHT }); if (!after) { this.portsOut.splice(0, 0, p); } return this.addPort(p); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.name = event.data.name; this.options.color = event.data.color; this.portsIn = _map(event.data.portsInOrder, (id) => { return this.getPortFromID(id); }) as DefaultPortModel[]; this.portsOut = _map(event.data.portsOutOrder, (id) => { return this.getPortFromID(id); }) as DefaultPortModel[]; } serialize(): any { return { ...super.serialize(), name: this.options.name, color: this.options.color, portsInOrder: _map(this.portsIn, (port) => { return port.getID(); }), portsOutOrder: _map(this.portsOut, (port) => { return port.getID(); }) }; } getInPorts(): DefaultPortModel[] { return this.portsIn; } getOutPorts(): DefaultPortModel[] { return this.portsOut; } } ================================================ FILE: packages/react-diagrams-defaults/src/node/DefaultNodeWidget.tsx ================================================ import * as React from 'react'; import _map from 'lodash/map'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { DefaultNodeModel } from './DefaultNodeModel'; import { DefaultPortLabel } from '../port/DefaultPortLabelWidget'; import styled from '@emotion/styled'; namespace S { export const Node = styled.div<{ background: string; selected: boolean }>` background-color: ${(p) => p.background}; border-radius: 5px; font-family: sans-serif; color: white; border: solid 2px black; overflow: visible; font-size: 11px; border: solid 2px ${(p) => (p.selected ? 'rgb(0,192,255)' : 'black')}; `; export const Title = styled.div` background: rgba(0, 0, 0, 0.3); display: flex; white-space: nowrap; justify-items: center; `; export const TitleName = styled.div` flex-grow: 1; padding: 5px 5px; `; export const Ports = styled.div` display: flex; background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.2)); `; export const PortsContainer = styled.div` flex-grow: 1; display: flex; flex-direction: column; &:first-of-type { margin-right: 10px; } &:only-child { margin-right: 0px; } `; } export interface DefaultNodeProps { node: DefaultNodeModel; engine: DiagramEngine; } /** * Default node that models the DefaultNodeModel. It creates two columns * for both all the input ports on the left, and the output ports on the right. */ export class DefaultNodeWidget extends React.Component { generatePort = (port) => { return ; }; render() { return ( {this.props.node.getOptions().name} {_map(this.props.node.getInPorts(), this.generatePort)} {_map(this.props.node.getOutPorts(), this.generatePort)} ); } } ================================================ FILE: packages/react-diagrams-defaults/src/port/DefaultPortFactory.tsx ================================================ import { DefaultPortModel } from './DefaultPortModel'; import { AbstractModelFactory } from '@projectstorm/react-canvas-core'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; export class DefaultPortFactory extends AbstractModelFactory { constructor() { super('default'); } generateModel(): DefaultPortModel { return new DefaultPortModel({ name: 'unknown' }); } } ================================================ FILE: packages/react-diagrams-defaults/src/port/DefaultPortLabelWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine, PortWidget } from '@projectstorm/react-diagrams-core'; import { DefaultPortModel } from './DefaultPortModel'; import styled from '@emotion/styled'; export interface DefaultPortLabelProps { port: DefaultPortModel; engine: DiagramEngine; } namespace S { export const PortLabel = styled.div` display: flex; margin-top: 1px; align-items: center; `; export const Label = styled.div` padding: 0 5px; flex-grow: 1; `; export const Port = styled.div` width: 15px; height: 15px; background: rgba(255, 255, 255, 0.1); &:hover { background: rgb(192, 255, 0); } `; } export class DefaultPortLabel extends React.Component { render() { const port = ( ); const label = {this.props.port.getOptions().label}; return ( {this.props.port.getOptions().in ? port : label} {this.props.port.getOptions().in ? label : port} ); } } ================================================ FILE: packages/react-diagrams-defaults/src/port/DefaultPortModel.ts ================================================ import { LinkModel, PortModel, PortModelAlignment, PortModelGenerics, PortModelOptions } from '@projectstorm/react-diagrams-core'; import { DefaultLinkModel } from '../link/DefaultLinkModel'; import { AbstractModelFactory, DeserializeEvent } from '@projectstorm/react-canvas-core'; export interface DefaultPortModelOptions extends PortModelOptions { label?: string; in?: boolean; type?: string; } export interface DefaultPortModelGenerics extends PortModelGenerics { OPTIONS: DefaultPortModelOptions; } export class DefaultPortModel extends PortModel { constructor(isIn: boolean, name?: string, label?: string); constructor(options: DefaultPortModelOptions); constructor(options: DefaultPortModelOptions | boolean, name?: string, label?: string) { if (!!name) { options = { in: !!options, name: name, label: label }; } options = options as DefaultPortModelOptions; super({ label: options.label || options.name, alignment: options.in ? PortModelAlignment.LEFT : PortModelAlignment.RIGHT, type: 'default', ...options }); } deserialize(event: DeserializeEvent) { super.deserialize(event); this.options.in = event.data.in; this.options.label = event.data.label; } serialize() { return { ...super.serialize(), in: this.options.in, label: this.options.label }; } link(port: PortModel, factory?: AbstractModelFactory): T { let link = this.createLinkModel(factory); link.setSourcePort(this); link.setTargetPort(port); return link as T; } canLinkToPort(port: PortModel): boolean { if (port instanceof DefaultPortModel) { return this.options.in !== port.getOptions().in; } return true; } createLinkModel(factory?: AbstractModelFactory): LinkModel { let link = super.createLinkModel(); if (!link && factory) { return factory.generateModel({}); } return link || new DefaultLinkModel(); } } ================================================ FILE: packages/react-diagrams-defaults/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "outDir": "dist", "rootDir": "src", "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ { "path": "../react-diagrams-core" } ] } ================================================ FILE: packages/react-diagrams-defaults/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: 'projectstorm/react-diagrams-defaults' } }; ================================================ FILE: packages/react-diagrams-routing/.npmignore ================================================ * !dist/**/* !package.json dist/tsconfig.tsbuildinfo ================================================ FILE: packages/react-diagrams-routing/CHANGELOG.md ================================================ # @projectstorm/react-diagrams-routing ## 7.1.3 ### Patch Changes - 80285fe: refactor: update lodash imports to use individual functions - Updated dependencies [09ed60f] - Updated dependencies [20766f5] - Updated dependencies [80285fe] - @projectstorm/react-canvas-core@7.0.3 - @projectstorm/react-diagrams-core@7.0.3 - @projectstorm/react-diagrams-defaults@7.1.3 - @projectstorm/geometry@7.0.3 ## 7.1.2 ### Patch Changes - 66c687a: Upgrade all dependencies and fix Storybook after upgrade - Updated dependencies [66c687a] - @projectstorm/react-diagrams-defaults@7.1.2 - @projectstorm/react-diagrams-core@7.0.2 - @projectstorm/react-canvas-core@7.0.2 - @projectstorm/geometry@7.0.2 ## 7.1.1 ### Patch Changes - b8a4cbd: Inline sources in sourcemap - Updated dependencies [b8a4cbd] - @projectstorm/geometry@7.0.1 - @projectstorm/react-canvas-core@7.0.1 - @projectstorm/react-diagrams-core@7.0.1 - @projectstorm/react-diagrams-defaults@7.1.1 ## 7.1.0 ### Minor Changes - e0d21f1: - [feature] new ability to refresh links in auto distribute system [PR 756](https://github.com/projectstorm/react-diagrams/pull/756) - [fix] Default link now uses the correct method for creating a point allowing this to be overridden [PR 939](https://github.com/projectstorm/react-diagrams/pull/939) Big thanks to @ToTheHit and @h0111in for your help on these, even though its very delayed on my part :) ### Patch Changes - Updated dependencies [e0d21f1] - @projectstorm/react-diagrams-defaults@7.1.0 ## 7.0.0 ### Major Changes - b051697: - [internal] moves to `Pnpm` (instead of yarn -\_-) - [internal]moves to `Changesets` for releases - [internal]removes `Lerna` - [internal] upgrades all dependencies - [internal] switches to workspace protocol syntax (Changesets will bake in the correct version when a publish occurs) - [internal] Changesets will open a release PR which can wrap up several changes in 1 go - [internal] Changesets will run the storybook deploy automatically upon merging the release PR - [internal] removes a lot of the stuff from the root package.json - [internal] cleans up the build and clean commands - [internal] remove E2E tests, they are a nightmare to maintain and the ROI is far too low - [fix] Wrong type name for react-canvas model listener - [fix] export more stuff form the main react-diagrams package - [fix] circular deps with Rectangle and Polygon (turns out this was a problem but only with UMD builds, sorry @everyone who I doubted, but this is also why I could never reproduce the issue) - [breaking change] compile both ES6 and UMD - [breaking change] moves dependencies back to each package. (After years of working on libraries, I've come to actually hate peer dependencies, and this is easily solved with build systems / package managers). - [breaking change] static methods on `Polygon` and `Rectangle` moved to standalone methods - [breaking change] static construction methods to rather deal with different Rectangle constructor overloads (I now consider this bad design) - [breaking change] introduce `Bounds` as a simpler point-array type to deal with boundary computation instead ### Patch Changes - Updated dependencies [b051697] - @projectstorm/react-diagrams-defaults@7.0.0 - @projectstorm/react-diagrams-core@7.0.0 - @projectstorm/react-canvas-core@7.0.0 - @projectstorm/geometry@7.0.0 ================================================ FILE: packages/react-diagrams-routing/README.md ================================================ # Project STORM > React Diagrams > Dagre This package adds dagre integration for laying out nodes and links ================================================ FILE: packages/react-diagrams-routing/jest.config.js ================================================ const path = require('path'); module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest' }, roots: [path.join(__dirname, 'tests')], testMatch: ['**/*.test.{ts,tsx}'] }; ================================================ FILE: packages/react-diagrams-routing/package.json ================================================ { "name": "@projectstorm/react-diagrams-routing", "version": "7.1.3", "author": "dylanvorster", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/projectstorm/react-diagrams.git" }, "scripts": { "clean": "rimraf ./dist", "build": "../../node_modules/.bin/webpack", "test": "../../node_modules/.bin/jest" }, "publishConfig": { "access": "public" }, "keywords": [ "web", "diagram", "diagrams", "react", "typescript", "flowchart", "simple", "links", "nodes" ], "main": "./dist/index.umd.js", "module": "./dist/index.js", "typings": "./dist/@types/index", "dependencies": { "@projectstorm/geometry": "workspace:*", "@projectstorm/react-canvas-core": "workspace:*", "@projectstorm/react-diagrams-core": "workspace:*", "@projectstorm/react-diagrams-defaults": "workspace:*", "dagre": "^0.8.5", "lodash": "^4.17.21", "pathfinding": "^0.4.18", "paths-js": "^0.4.11", "react": "^19.0.0" }, "devDependencies": { "@types/dagre": "^0.7.50", "@types/lodash": "^4.14.200", "@types/react": "^19.0.12" } } ================================================ FILE: packages/react-diagrams-routing/src/dagre/DagreEngine.ts ================================================ import { DiagramModel, PointModel } from '@projectstorm/react-diagrams-core'; import * as dagre from 'dagre'; import { GraphLabel } from 'dagre'; import _every from 'lodash/every'; import _findIndex from 'lodash/findIndex'; import _forEach from 'lodash/forEach'; import _map from 'lodash/map'; import _range from 'lodash/range'; import _sortBy from 'lodash/sortBy'; import { Point } from '@projectstorm/geometry'; export interface DagreEngineOptions { graph?: GraphLabel; /** * Will also re-layout links */ includeLinks?: boolean; nodeMargin?: number; } export class DagreEngine { options: DagreEngineOptions; constructor(options: DagreEngineOptions = {}) { this.options = options; } redistribute(model: DiagramModel) { // Create a new directed graph var g = new dagre.graphlib.Graph({ multigraph: true, compound: true }); g.setGraph(this.options.graph || {}); g.setDefaultEdgeLabel(function () { return {}; }); // set nodes _forEach(model.getNodes(), (node) => { g.setNode(node.getID(), { width: node.width, height: node.height }); }); _forEach(model.getLinks(), (link) => { // set edges if (link.getSourcePort() && link.getTargetPort()) { g.setEdge({ v: link.getSourcePort().getNode().getID(), w: link.getTargetPort().getNode().getID(), name: link.getID() }); } }); // layout the graph dagre.layout(g); g.nodes().forEach((v) => { const node = g.node(v); model.getNode(v).setPosition(node.x - node.width / 2, node.y - node.height / 2); }); // also include links? if (this.options.includeLinks) { g.edges().forEach((e) => { const edge = g.edge(e); const link = model.getLink(e.name); const points = [link.getFirstPoint()]; for (let i = 1; i < edge.points.length - 1; i++) { points.push(new PointModel({ link: link, position: new Point(edge.points[i].x, edge.points[i].y) })); } link.setPoints(points.concat(link.getLastPoint())); }); } } /** * TODO cleanup this method into smaller methods */ public refreshLinks(diagram: DiagramModel) { const { nodeMargin } = this.options; const nodes = diagram.getNodes(); const links = diagram.getLinks(); let maxChunkRowIndex = -1; // build the chunk matrix const chunks: { [id: number]: { [id: number]: boolean } } = {}; // true: occupied, false: blank const NodeXColumnIndexDictionary: { [id: number]: number } = {}; let verticalLines: number[] = []; _forEach(nodes, (node) => { // find vertical lines. vertical lines go through maximum number of nodes located under each other. const nodeColumnCenter = node.getX() + node.width / 2; if ( _every(verticalLines, (vLine) => { return Math.abs(nodeColumnCenter - vLine) > nodeMargin; }) ) { verticalLines.push(nodeColumnCenter); } }); // sort chunk columns verticalLines = verticalLines.sort((a, b) => a - b); _forEach(verticalLines, (line, index) => { chunks[index] = {}; chunks[index + 0.5] = {}; }); // set occupied chunks _forEach(nodes, (node) => { const nodeColumnCenter = node.getX() + node.width / 2; const startChunkIndex = Math.floor(node.getY() / nodeMargin); const endChunkIndex = Math.floor((node.getY() + node.height) / nodeMargin); // find max ChunkRowIndex if (endChunkIndex > maxChunkRowIndex) maxChunkRowIndex = endChunkIndex; const nodeColumnIndex = _findIndex(verticalLines, (vLine) => { return Math.abs(nodeColumnCenter - vLine) <= nodeMargin; }); _forEach(_range(startChunkIndex, endChunkIndex + 1), (chunkIndex) => { chunks[nodeColumnIndex][chunkIndex] = true; }); NodeXColumnIndexDictionary[node.getX()] = nodeColumnIndex; }); // sort links based on their distances const edges = _map(links, (link) => { if (link.getSourcePort() && link.getTargetPort()) { const source = link.getSourcePort().getNode(); const target = link.getTargetPort().getNode(); const sourceIndex = NodeXColumnIndexDictionary[source.getX()]; const targetIndex = NodeXColumnIndexDictionary[target.getX()]; return sourceIndex > targetIndex ? { link, sourceIndex, sourceY: source.getY() + source.height / 2, source, targetIndex, targetY: target.getY() + source.height / 2, target } : { link, sourceIndex: targetIndex, sourceY: target.getY() + target.height / 2, source: target, targetIndex: sourceIndex, targetY: source.getY() + source.height / 2, target: source }; } }); const sortedEdges = _sortBy(edges, (link) => { return Math.abs(link.targetIndex - link.sourceIndex); }); // set link points if (this.options.includeLinks) { _forEach(sortedEdges, (edge) => { const link = diagram.getLink(edge.link.getID()); // re-draw if (Math.abs(edge.sourceIndex - edge.targetIndex) > 1) { // get the length of link in column const columns = _range(edge.sourceIndex - 1, edge.targetIndex); const chunkIndex = Math.floor(edge.sourceY / nodeMargin); const targetChunkIndex = Math.floor(edge.targetY / nodeMargin); // check upper paths let northCost = 1; let aboveRowIndex = chunkIndex; for (; aboveRowIndex >= 0; aboveRowIndex--, northCost++) { if ( _every(columns, (columnIndex) => { return !( chunks[columnIndex][aboveRowIndex] || chunks[columnIndex + 0.5][aboveRowIndex] || chunks[columnIndex - 0.5][aboveRowIndex] ); }) ) { break; } } // check lower paths let southCost = 0; let belowRowIndex = chunkIndex; for (; belowRowIndex <= maxChunkRowIndex; belowRowIndex++, southCost++) { if ( _every(columns, (columnIndex) => { return !( chunks[columnIndex][belowRowIndex] || chunks[columnIndex + 0.5][belowRowIndex] || chunks[columnIndex - 0.5][belowRowIndex] ); }) ) { break; } } // pick the cheapest path const pathRowIndex = southCost + (belowRowIndex - targetChunkIndex) < northCost + (targetChunkIndex - aboveRowIndex) ? belowRowIndex + 1 : aboveRowIndex - 1; // Finally update the link points const points = [link.getFirstPoint()]; points.push( new PointModel({ link: link, position: new Point( (verticalLines[columns[0]] + verticalLines[columns[0] + 1]) / 2, (pathRowIndex + 0.5) * nodeMargin ) }) ); _forEach(columns, (column) => { points.push( new PointModel({ link: link, position: new Point(verticalLines[column], (pathRowIndex + 0.5) * nodeMargin) }) ); points.push( new PointModel({ link: link, position: new Point( (verticalLines[column] + verticalLines[column - 1]) / 2, (pathRowIndex + 0.5) * nodeMargin ) }) ); chunks[column][pathRowIndex] = true; chunks[column][pathRowIndex + 1] = true; chunks[column + 0.5][pathRowIndex] = true; chunks[column + 0.5][pathRowIndex + 1] = true; }); link.setPoints(points.concat(link.getLastPoint())); } else { // refresh link.setPoints([link.getFirstPoint(), link.getLastPoint()]); const columnIndex = (edge.sourceIndex + edge.targetIndex) / 2; if (!chunks[columnIndex]) { chunks[columnIndex] = {}; } const rowIndex = Math.floor((edge.sourceY + edge.targetY) / 2 / nodeMargin); chunks[columnIndex][rowIndex] = true; chunks[columnIndex][rowIndex + 1] = true; } }); } } } ================================================ FILE: packages/react-diagrams-routing/src/engine/PathFinding.ts ================================================ import * as PF from 'pathfinding'; import { PathFindingLinkFactory } from '../link/PathFindingLinkFactory'; import { PointModel } from '@projectstorm/react-diagrams-core'; /* it can be very expensive to calculate routes when every single pixel on the canvas is individually represented. Using the factor below, we combine values in order to achieve the best trade-off between accuracy and performance. */ const pathFinderInstance = new PF.JumpPointFinder({ heuristic: PF.Heuristic.manhattan, diagonalMovement: PF.DiagonalMovement.Never }); export class PathFinding { instance: any; factory: PathFindingLinkFactory; constructor(factory: PathFindingLinkFactory) { this.instance = pathFinderInstance; this.factory = factory; } /** * Taking as argument a fully unblocked walking matrix, this method * finds a direct path from point A to B. */ calculateDirectPath(from: PointModel, to: PointModel): number[][] { const matrix = this.factory.getCanvasMatrix(); const grid = new PF.Grid(matrix); return pathFinderInstance.findPath( this.factory.translateRoutingX(Math.floor(from.getX() / this.factory.ROUTING_SCALING_FACTOR)), this.factory.translateRoutingY(Math.floor(from.getY() / this.factory.ROUTING_SCALING_FACTOR)), this.factory.translateRoutingX(Math.floor(to.getX() / this.factory.ROUTING_SCALING_FACTOR)), this.factory.translateRoutingY(Math.floor(to.getY() / this.factory.ROUTING_SCALING_FACTOR)), grid ); } /** * Using @link{#calculateDirectPath}'s result as input, we here * determine the first walkable point found in the matrix that includes * blocked paths. */ calculateLinkStartEndCoords( matrix: number[][], path: number[][] ): { start: { x: number; y: number; }; end: { x: number; y: number; }; pathToStart: number[][]; pathToEnd: number[][]; } { const startIndex = path.findIndex((point) => { if (matrix[point[1]]) return matrix[point[1]][point[0]] === 0; else return false; }); const endIndex = path.length - 1 - path .slice() .reverse() .findIndex((point) => { if (matrix[point[1]]) return matrix[point[1]][point[0]] === 0; else return false; }); // are we trying to create a path exclusively through blocked areas? // if so, let's fallback to the linear routing if (startIndex === -1 || endIndex === -1) { return undefined; } const pathToStart = path.slice(0, startIndex); const pathToEnd = path.slice(endIndex); return { start: { x: path[startIndex][0], y: path[startIndex][1] }, end: { x: path[endIndex][0], y: path[endIndex][1] }, pathToStart, pathToEnd }; } /** * Puts everything together: merges the paths from/to the centre of the ports, * with the path calculated around other elements. */ calculateDynamicPath( routingMatrix: number[][], start: { x: number; y: number; }, end: { x: number; y: number; }, pathToStart: number[][], pathToEnd: number[][] ) { // generate the path based on the matrix with obstacles const grid = new PF.Grid(routingMatrix); const dynamicPath = pathFinderInstance.findPath(start.x, start.y, end.x, end.y, grid); // aggregate everything to have the calculated path ready for rendering const pathCoords = pathToStart .concat(dynamicPath, pathToEnd) .map((coords) => [ this.factory.translateRoutingX(coords[0], true), this.factory.translateRoutingY(coords[1], true) ]); return PF.Util.compressPath(pathCoords); } } ================================================ FILE: packages/react-diagrams-routing/src/index.ts ================================================ export * from './link/PathFindingLinkFactory'; export * from './link/PathFindingLinkModel'; export * from './link/PathFindingLinkWidget'; export * from './link/RightAngleLinkWidget'; export * from './link/RightAngleLinkFactory'; export * from './link/RightAngleLinkModel'; export * from './engine/PathFinding'; export * from './dagre/DagreEngine'; ================================================ FILE: packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx ================================================ import * as React from 'react'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { PathFindingLinkModel } from './PathFindingLinkModel'; import { PathFindingLinkWidget } from './PathFindingLinkWidget'; import _cloneDeep from 'lodash/cloneDeep'; import _concat from 'lodash/concat'; import _defer from 'lodash/defer'; import _flatMap from 'lodash/flatMap'; import _get from 'lodash/get'; import _minBy from 'lodash/minBy'; import _maxBy from 'lodash/maxBy'; import _range from 'lodash/range'; import _reduce from 'lodash/reduce'; import _values from 'lodash/values'; import * as Path from 'paths-js/path'; import { DefaultLinkFactory } from '@projectstorm/react-diagrams-defaults'; import { AbstractDisplacementState, AbstractFactory, Action, FactoryBank, InputType, ListenerHandle } from '@projectstorm/react-canvas-core'; import { JSX } from 'react'; export class PathFindingLinkFactory extends DefaultLinkFactory { ROUTING_SCALING_FACTOR: number = 5; // calculated only when smart routing is active canvasMatrix: number[][] = []; routingMatrix: number[][] = []; // used when at least one element has negative coordinates hAdjustmentFactor: number = 0; vAdjustmentFactor: number = 0; static NAME = 'pathfinding'; listener: ListenerHandle; constructor() { super(PathFindingLinkFactory.NAME); } setDiagramEngine(engine: DiagramEngine): void { super.setDiagramEngine(engine); // listen for drag changes engine.getStateMachine().registerListener({ stateChanged: (event) => { if (event.newState instanceof AbstractDisplacementState) { const deRegister = engine.getActionEventBus().registerAction( new Action({ type: InputType.MOUSE_UP, fire: () => { this.calculateRoutingMatrix(); engine.repaintCanvas(); deRegister(); } }) ); } } }); this.listener = engine.registerListener({ canvasReady: () => { _defer(() => { this.calculateRoutingMatrix(); engine.repaintCanvas(); }); } }); } setFactoryBank(bank: FactoryBank): void { super.setFactoryBank(bank); if (!bank && this.listener) { this.listener.deregister(); } } generateReactWidget(event): JSX.Element { return ; } generateModel(event): PathFindingLinkModel { return new PathFindingLinkModel(); } /** * A representation of the canvas in the following format: * * +-----------------+ * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * +-----------------+ * * In which all walkable points are marked by zeros. * It uses @link{#ROUTING_SCALING_FACTOR} to reduce the matrix dimensions and improve performance. */ getCanvasMatrix(): number[][] { if (this.canvasMatrix.length === 0) { this.calculateCanvasMatrix(); } return this.canvasMatrix; } calculateCanvasMatrix() { const { width: canvasWidth, hAdjustmentFactor, height: canvasHeight, vAdjustmentFactor } = this.calculateMatrixDimensions(); this.hAdjustmentFactor = hAdjustmentFactor; this.vAdjustmentFactor = vAdjustmentFactor; const matrixWidth = Math.ceil(canvasWidth / this.ROUTING_SCALING_FACTOR); const matrixHeight = Math.ceil(canvasHeight / this.ROUTING_SCALING_FACTOR); this.canvasMatrix = _range(0, matrixHeight).map(() => { return new Array(matrixWidth).fill(0); }); } /** * A representation of the canvas in the following format: * * +-----------------+ * | 0 0 1 1 0 0 0 0 | * | 0 0 1 1 0 0 1 1 | * | 0 0 0 0 0 0 1 1 | * | 1 1 0 0 0 0 0 0 | * | 1 1 0 0 0 0 0 0 | * +-----------------+ * * In which all points blocked by a node (and its ports) are * marked as 1; points were there is nothing (ie, free) receive 0. */ getRoutingMatrix(): number[][] { if (this.routingMatrix.length === 0) { this.calculateRoutingMatrix(); } return this.routingMatrix; } calculateRoutingMatrix(): void { const matrix = _cloneDeep(this.getCanvasMatrix()); // nodes need to be marked as blocked points this.markNodes(matrix); // same thing for ports this.markPorts(matrix); this.routingMatrix = matrix; } /** * The routing matrix does not have negative indexes, but elements could be negatively positioned. * We use the functions below to translate back and forth between these coordinates, relying on the * calculated values of hAdjustmentFactor and vAdjustmentFactor. */ translateRoutingX(x: number, reverse: boolean = false) { return x + this.hAdjustmentFactor * (reverse ? -1 : 1); } translateRoutingY(y: number, reverse: boolean = false) { return y + this.vAdjustmentFactor * (reverse ? -1 : 1); } /** * Despite being a long method, we simply iterate over all three collections (nodes, ports and points) * to find the highest X and Y dimensions, so we can build the matrix large enough to contain all elements. */ calculateMatrixDimensions = (): { width: number; hAdjustmentFactor: number; height: number; vAdjustmentFactor: number; } => { const allNodesCoords = _values(this.engine.getModel().getNodes()).map((item) => ({ x: item.getX(), width: item.width, y: item.getY(), height: item.height })); const allLinks = _values(this.engine.getModel().getLinks()); const allPortsCoords = _flatMap(allLinks.map((link) => [link.getSourcePort(), link.getTargetPort()])) .filter((port) => port !== null) .map((item) => ({ x: item.getX(), width: item.width, y: item.getY(), height: item.height })); const allPointsCoords = _flatMap(allLinks.map((link) => link.getPoints())).map((item) => ({ // points don't have width/height, so let's just use 0 x: item.getX(), width: 0, y: item.getY(), height: 0 })); const sumProps = (object, props) => _reduce(props, (acc, prop) => acc + _get(object, prop, 0), 0); const canvas = this.engine.getCanvas() as HTMLDivElement; const concatedCoords = _concat(allNodesCoords, allPortsCoords, allPointsCoords); const minX = Math.floor(Math.min(_get(_minBy(concatedCoords, 'x'), 'x', 0), 0) / this.ROUTING_SCALING_FACTOR) * this.ROUTING_SCALING_FACTOR; const maxXElement = _maxBy(concatedCoords, (item) => sumProps(item, ['x', 'width'])); const maxX = Math.max(sumProps(maxXElement, ['x', 'width']), canvas.offsetWidth); const minYCoords = _minBy(concatedCoords, 'y'); const minY = Math.floor(Math.min(_get(minYCoords, 'y', 0), 0) / this.ROUTING_SCALING_FACTOR) * this.ROUTING_SCALING_FACTOR; const maxYElement = _maxBy(concatedCoords, (item) => sumProps(item, ['y', 'height'])); const maxY = Math.max(sumProps(maxYElement, ['y', 'height']), canvas.offsetHeight); return { width: Math.ceil(Math.abs(minX) + maxX), hAdjustmentFactor: Math.abs(minX) / this.ROUTING_SCALING_FACTOR + 1, height: Math.ceil(Math.abs(minY) + maxY), vAdjustmentFactor: Math.abs(minY) / this.ROUTING_SCALING_FACTOR + 1 }; }; /** * Updates (by reference) where nodes will be drawn on the matrix passed in. */ markNodes = (matrix: number[][]): void => { _values(this.engine.getModel().getNodes()).forEach((node) => { const startX = Math.floor(node.getX() / this.ROUTING_SCALING_FACTOR); const endX = Math.ceil((node.getX() + node.width) / this.ROUTING_SCALING_FACTOR); const startY = Math.floor(node.getY() / this.ROUTING_SCALING_FACTOR); const endY = Math.ceil((node.getY() + node.height) / this.ROUTING_SCALING_FACTOR); for (let x = startX - 1; x <= endX + 1; x++) { for (let y = startY - 1; y < endY + 1; y++) { this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y)); } } }); }; /** * Updates (by reference) where ports will be drawn on the matrix passed in. */ markPorts = (matrix: number[][]): void => { const allElements = _flatMap( _values(this.engine.getModel().getLinks()).map((link) => [].concat(link.getSourcePort(), link.getTargetPort())) ); allElements .filter((port) => port !== null) .forEach((port) => { const startX = Math.floor(port.x / this.ROUTING_SCALING_FACTOR); const endX = Math.ceil((port.x + port.width) / this.ROUTING_SCALING_FACTOR); const startY = Math.floor(port.y / this.ROUTING_SCALING_FACTOR); const endY = Math.ceil((port.y + port.height) / this.ROUTING_SCALING_FACTOR); for (let x = startX - 1; x <= endX + 1; x++) { for (let y = startY - 1; y < endY + 1; y++) { this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y)); } } }); }; markMatrixPoint = (matrix: number[][], x: number, y: number) => { if (matrix[y] !== undefined && matrix[y][x] !== undefined) { matrix[y][x] = 1; } }; generateDynamicPath(pathCoords: number[][]) { let path = Path(); path = path.moveto(pathCoords[0][0] * this.ROUTING_SCALING_FACTOR, pathCoords[0][1] * this.ROUTING_SCALING_FACTOR); pathCoords.slice(1).forEach((coords) => { path = path.lineto(coords[0] * this.ROUTING_SCALING_FACTOR, coords[1] * this.ROUTING_SCALING_FACTOR); }); return path.print(); } } ================================================ FILE: packages/react-diagrams-routing/src/link/PathFindingLinkModel.ts ================================================ import { PathFindingLinkFactory } from './PathFindingLinkFactory'; import { DefaultLinkModel, DefaultLinkModelOptions } from '@projectstorm/react-diagrams-defaults'; export class PathFindingLinkModel extends DefaultLinkModel { constructor(options: DefaultLinkModelOptions = {}) { super({ type: PathFindingLinkFactory.NAME, ...options }); } performanceTune() { return false; } } ================================================ FILE: packages/react-diagrams-routing/src/link/PathFindingLinkWidget.tsx ================================================ import * as React from 'react'; import _first from 'lodash/first'; import _last from 'lodash/last'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { PathFinding } from '../engine/PathFinding'; import { PathFindingLinkFactory } from './PathFindingLinkFactory'; import { PathFindingLinkModel } from './PathFindingLinkModel'; import { DefaultLinkSegmentWidget } from '@projectstorm/react-diagrams-defaults'; import { JSX } from 'react'; export interface PathFindingLinkWidgetProps { color?: string; width?: number; smooth?: boolean; link: PathFindingLinkModel; diagramEngine: DiagramEngine; factory: PathFindingLinkFactory; } export interface PathFindingLinkWidgetState { selected: boolean; } export class PathFindingLinkWidget extends React.Component { refPaths: React.RefObject[]; pathFinding: PathFinding; constructor(props: PathFindingLinkWidgetProps) { super(props); this.refPaths = []; this.state = { selected: false }; this.pathFinding = new PathFinding(this.props.factory); } componentDidUpdate(): void { this.props.link.setRenderedPaths( this.refPaths.map((ref) => { return ref.current; }) ); } componentDidMount(): void { this.props.link.setRenderedPaths( this.refPaths.map((ref) => { return ref.current; }) ); } componentWillUnmount(): void { this.props.link.setRenderedPaths([]); } generateLink(path: string, id: string | number): JSX.Element { const ref = React.createRef(); this.refPaths.push(ref); return ( { this.setState({ selected: selected }); }} extras={{}} /> ); } render() { this.refPaths = []; //ensure id is present for all points on the path var points = this.props.link.getPoints(); var paths = []; // first step: calculate a direct path between the points being linked const directPathCoords = this.pathFinding.calculateDirectPath(_first(points), _last(points)); const routingMatrix = this.props.factory.getRoutingMatrix(); // now we need to extract, from the routing matrix, the very first walkable points // so they can be used as origin and destination of the link to be created const smartLink = this.pathFinding.calculateLinkStartEndCoords(routingMatrix, directPathCoords); if (smartLink) { const { start, end, pathToStart, pathToEnd } = smartLink; // second step: calculate a path avoiding hitting other elements const simplifiedPath = this.pathFinding.calculateDynamicPath(routingMatrix, start, end, pathToStart, pathToEnd); paths.push( //smooth: boolean, extraProps: any, id: string | number, firstPoint: PointModel, lastPoint: PointModel this.generateLink(this.props.factory.generateDynamicPath(simplifiedPath), '0') ); } return <>{paths}; } } ================================================ FILE: packages/react-diagrams-routing/src/link/RightAngleLinkFactory.tsx ================================================ import * as React from 'react'; import { RightAngleLinkWidget } from './RightAngleLinkWidget'; import { DefaultLinkFactory } from '@projectstorm/react-diagrams-defaults'; import { RightAngleLinkModel } from './RightAngleLinkModel'; import { JSX } from 'react'; /** * @author Daniel Lazar */ export class RightAngleLinkFactory extends DefaultLinkFactory { static NAME = 'rightAngle'; constructor() { super(RightAngleLinkFactory.NAME); } generateModel(event): RightAngleLinkModel { return new RightAngleLinkModel(); } generateReactWidget(event): JSX.Element { return ; } } ================================================ FILE: packages/react-diagrams-routing/src/link/RightAngleLinkModel.ts ================================================ import { DefaultLinkModel, DefaultLinkModelOptions } from '@projectstorm/react-diagrams-defaults'; import { RightAngleLinkFactory } from './RightAngleLinkFactory'; import { PointModel } from '@projectstorm/react-diagrams-core'; import { DeserializeEvent } from '@projectstorm/react-canvas-core'; export class RightAngleLinkModel extends DefaultLinkModel { lastHoverIndexOfPath: number; private _lastPathXdirection: boolean; private _firstPathXdirection: boolean; constructor(options: DefaultLinkModelOptions = {}) { super({ type: RightAngleLinkFactory.NAME, ...options }); this.lastHoverIndexOfPath = 0; this._lastPathXdirection = false; this._firstPathXdirection = false; } setFirstAndLastPathsDirection() { let points = this.getPoints(); for (let i = 1; i < points.length; i += points.length - 2) { let dx = Math.abs(points[i].getX() - points[i - 1].getX()); let dy = Math.abs(points[i].getY() - points[i - 1].getY()); if (i - 1 === 0) { this._firstPathXdirection = dx > dy; } else { this._lastPathXdirection = dx > dy; } } } // @ts-ignore addPoint

(pointModel: P, index: number = 1): P { // @ts-ignore super.addPoint(pointModel, index); this.setFirstAndLastPathsDirection(); return pointModel; } deserialize(event: DeserializeEvent) { super.deserialize(event); this.setFirstAndLastPathsDirection(); } setManuallyFirstAndLastPathsDirection(first, last) { this._firstPathXdirection = first; this._lastPathXdirection = last; } getLastPathXdirection(): boolean { return this._lastPathXdirection; } getFirstPathXdirection(): boolean { return this._firstPathXdirection; } setWidth(width: number) { this.options.width = width; this.fireEvent({ width }, 'widthChanged'); } setColor(color: string) { this.options.color = color; this.fireEvent({ color }, 'colorChanged'); } } ================================================ FILE: packages/react-diagrams-routing/src/link/RightAngleLinkWidget.tsx ================================================ import * as React from 'react'; import { DiagramEngine, LinkWidget, PointModel } from '@projectstorm/react-diagrams-core'; import { RightAngleLinkFactory } from './RightAngleLinkFactory'; import { DefaultLinkModel, DefaultLinkSegmentWidget } from '@projectstorm/react-diagrams-defaults'; import { Point } from '@projectstorm/geometry'; import { JSX, MouseEvent } from 'react'; import { RightAngleLinkModel } from './RightAngleLinkModel'; export interface RightAngleLinkProps { color?: string; width?: number; smooth?: boolean; link: RightAngleLinkModel; diagramEngine: DiagramEngine; factory: RightAngleLinkFactory; } export interface RightAngleLinkState { selected: boolean; canDrag: boolean; } export class RightAngleLinkWidget extends React.Component { public static defaultProps: RightAngleLinkProps = { color: 'red', width: 3, link: null, smooth: false, diagramEngine: null, factory: null }; refPaths: React.RefObject[]; // DOM references to the label and paths (if label is given), used to calculate dynamic positioning refLabels: { [id: string]: HTMLElement }; dragging_index: number; constructor(props: RightAngleLinkProps) { super(props); this.refPaths = []; this.state = { selected: false, canDrag: false }; this.dragging_index = 0; } componentDidUpdate(): void { this.props.link.setRenderedPaths( this.refPaths.map((ref) => { return ref.current; }) ); } componentDidMount(): void { this.props.link.setRenderedPaths( this.refPaths.map((ref) => { return ref.current; }) ); } componentWillUnmount(): void { this.props.link.setRenderedPaths([]); } generateLink(path: string, extraProps: any, id: string | number): JSX.Element { const ref = React.createRef(); this.refPaths.push(ref); return ( { this.setState({ selected: selected }); }} extras={extraProps} /> ); } calculatePositions(points: PointModel[], event: MouseEvent, index: number, coordinate: string) { // If path is first or last add another point to keep node port on its position if (index === 0) { let point = new PointModel({ link: this.props.link, position: new Point(points[index].getX(), points[index].getY()) }); this.props.link.addPoint(point, index); this.dragging_index++; return; } else if (index === points.length - 2) { let point = new PointModel({ link: this.props.link, position: new Point(points[index + 1].getX(), points[index + 1].getY()) }); this.props.link.addPoint(point, index + 1); return; } // Merge two points if it is not close to node port and close to each other if (index - 2 > 0) { let _points = { [index - 2]: points[index - 2].getPosition(), [index + 1]: points[index + 1].getPosition(), [index - 1]: points[index - 1].getPosition() }; if (Math.abs(_points[index - 1][coordinate] - _points[index + 1][coordinate]) < 5) { _points[index - 2][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; _points[index + 1][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; points[index - 2].setPosition(_points[index - 2]); points[index + 1].setPosition(_points[index + 1]); points[index - 1].remove(); points[index - 1].remove(); this.dragging_index--; this.dragging_index--; return; } } // Merge two points if it is not close to node port if (index + 2 < points.length - 2) { let _points = { [index + 3]: points[index + 3].getPosition(), [index + 2]: points[index + 2].getPosition(), [index + 1]: points[index + 1].getPosition(), [index]: points[index].getPosition() }; if (Math.abs(_points[index + 1][coordinate] - _points[index + 2][coordinate]) < 5) { _points[index][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; _points[index + 3][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; points[index].setPosition(_points[index]); points[index + 3].setPosition(_points[index + 3]); points[index + 1].remove(); points[index + 1].remove(); return; } } // If no condition above handled then just update path points position let _points = { [index]: points[index].getPosition(), [index + 1]: points[index + 1].getPosition() }; _points[index][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; _points[index + 1][coordinate] = this.props.diagramEngine.getRelativeMousePoint(event)[coordinate]; points[index].setPosition(_points[index]); points[index + 1].setPosition(_points[index + 1]); } draggingEvent(event: MouseEvent, index: number) { let points = this.props.link.getPoints(); // get moving difference. Index + 1 will work because links indexes has // length = points.lenght - 1 let dx = Math.abs(points[index].getX() - points[index + 1].getX()); let dy = Math.abs(points[index].getY() - points[index + 1].getY()); // moving with y direction if (dx === 0) { this.calculatePositions(points, event, index, 'x'); } else if (dy === 0) { this.calculatePositions(points, event, index, 'y'); } this.props.link.setFirstAndLastPathsDirection(); } handleMove = function (event: MouseEvent) { this.draggingEvent(event, this.dragging_index); }.bind(this); handleUp = function (event: MouseEvent) { // Unregister handlers to avoid multiple event handlers for other links this.setState({ canDrag: false, selected: false }); window.removeEventListener('mousemove', this.handleMove); window.removeEventListener('mouseup', this.handleUp); }.bind(this); render() { //ensure id is present for all points on the path let points = this.props.link.getPoints(); let paths = []; this.refPaths = []; // Get points based on link orientation let pointLeft = points[0]; let pointRight = points[points.length - 1]; let hadToSwitch = false; if (pointLeft.getX() > pointRight.getX()) { pointLeft = points[points.length - 1]; pointRight = points[0]; hadToSwitch = true; } let dy = Math.abs(points[0].getY() - points[points.length - 1].getY()); // When new link add one middle point to get everywhere 90° angle if (this.props.link.getTargetPort() === null && points.length === 2) { [...Array(2)].forEach((item) => { this.props.link.addPoint( new PointModel({ link: this.props.link, position: new Point(pointLeft.getX(), pointRight.getY()) }), 1 ); }); this.props.link.setManuallyFirstAndLastPathsDirection(true, true); } // When new link is moving and not connected to target port move with middle point // TODO: @DanielLazarLDAPPS This will be better to update in DragNewLinkState // in function fireMouseMoved to avoid calling this unexpectedly e.g. after Deserialize else if (this.props.link.getTargetPort() === null && this.props.link.getSourcePort() !== null) { points[1].setPosition( pointRight.getX() + (pointLeft.getX() - pointRight.getX()) / 2, !hadToSwitch ? pointLeft.getY() : pointRight.getY() ); points[2].setPosition( pointRight.getX() + (pointLeft.getX() - pointRight.getX()) / 2, !hadToSwitch ? pointRight.getY() : pointLeft.getY() ); } // Render was called but link is not moved but user. // Node is moved and in this case fix coordinates to get 90° angle. // For loop just for first and last path else if (!this.state.canDrag && points.length > 2) { // Those points and its position only will be moved for (let i = 1; i < points.length; i += points.length - 2) { if (i - 1 === 0) { if (this.props.link.getFirstPathXdirection()) { points[i].setPosition(points[i].getX(), points[i - 1].getY()); } else { points[i].setPosition(points[i - 1].getX(), points[i].getY()); } } else { if (this.props.link.getLastPathXdirection()) { points[i - 1].setPosition(points[i - 1].getX(), points[i].getY()); } else { points[i - 1].setPosition(points[i].getX(), points[i - 1].getY()); } } } } // If there is existing link which has two points add one // NOTE: It doesn't matter if check is for dy or dx if (points.length === 2 && dy !== 0 && !this.state.canDrag) { this.props.link.addPoint( new PointModel({ link: this.props.link, position: new Point(pointLeft.getX(), pointRight.getY()) }) ); } for (let j = 0; j < points.length - 1; j++) { paths.push( this.generateLink( LinkWidget.generateLinePath(points[j], points[j + 1]), { 'data-linkid': this.props.link.getID(), 'data-point': j, onMouseDown: (event: MouseEvent) => { if (event.button === 0) { this.setState({ canDrag: true }); this.dragging_index = j; // Register mouse move event to track mouse position // On mouse up these events are unregistered check "this.handleUp" window.addEventListener('mousemove', this.handleMove); window.addEventListener('mouseup', this.handleUp); } }, onMouseEnter: (event: MouseEvent) => { this.setState({ selected: true }); this.props.link.lastHoverIndexOfPath = j; } }, j ) ); } return {paths}; } } ================================================ FILE: packages/react-diagrams-routing/tests/PathFinding.test.tsx ================================================ import { PathFinding } from '../src/engine/PathFinding'; describe('calculating start and end points', function () { let pathFinding: PathFinding = new PathFinding(null); beforeEach(() => { pathFinding = new PathFinding(null); }); test('return correct object for valid walkable input', () => { const matrix = [ [0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0] ]; const path = [ [0, 5], [1, 4], [2, 3], [3, 2], [4, 1], [5, 0] ]; const result = pathFinding.calculateLinkStartEndCoords(matrix, path); expect(result.start).toEqual({ x: 2, y: 3 }); expect(result.end).toEqual({ x: 3, y: 2 }); expect(result.pathToStart).toEqual([ [0, 5], [1, 4] ]); expect(result.pathToEnd).toEqual([ [3, 2], [4, 1], [5, 0] ]); }); test('undefined is returned when no walkable path exists', () => { const matrix = [ [0, 0, 1, 1], [0, 0, 1, 1], [1, 1, 0, 0], [1, 1, 0, 0] ]; const path = [ [0, 3], [1, 2], [2, 1], [3, 0] ]; const result = pathFinding.calculateLinkStartEndCoords(matrix, path); expect(result).toBeUndefined(); }); }); ================================================ FILE: packages/react-diagrams-routing/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "outDir": "dist", "rootDir": "src", "declarationDir": "dist/@types", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "include": ["./src"], "references": [ { "path": "../geometry" }, { "path": "../react-canvas-core" }, { "path": "../react-diagrams-defaults" } ] } ================================================ FILE: packages/react-diagrams-routing/webpack.config.js ================================================ const config = require('../../webpack.shared')(__dirname); module.exports = { ...config, output: { ...config.output, library: 'projectstorm/react-diagrams-routing' } }; ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - 'packages/*' - 'diagrams-demo-gallery' - 'diagrams-demo-project' ================================================ FILE: tsconfig.base.json ================================================ { "compileOnSave": false, "compilerOptions": { "declaration": true, "composite": true, "incremental": true, "strictNullChecks": false, "sourceMap": true, "inlineSources": true, "skipLibCheck": true, "jsx": "react", "target": "ES6", "moduleResolution": "Node", "module": "es6", "strict": false, "lib": [ "DOM", "ES6" ] } } ================================================ FILE: tsconfig.json ================================================ { "files": [], "references": [ { "path": "./packages/geometry" }, { "path": "./packages/react-canvas-core" }, { "path": "./packages/react-diagrams" }, { "path": "./packages/react-diagrams-core" }, { "path": "./packages/react-diagrams-defaults" }, { "path": "./packages/react-diagrams-routing" } ] } ================================================ FILE: webpack.shared.js ================================================ const production = process.env.NODE_ENV === 'production'; const TerserPlugin = require('terser-webpack-plugin'); const nodeExternals = require('webpack-node-externals'); const path = require('path'); module.exports = (directory) => { return { entry: path.join(directory, './dist/index.js'), output: { filename: 'index.umd.js', path: path.join(directory, 'dist'), libraryTarget: 'umd' }, externals: [ nodeExternals({ modulesDir: path.join(directory, 'node_modules') }), nodeExternals({ modulesDir: path.join(__dirname, 'node_modules') }) ], module: { rules: [ { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' } ] }, resolve: { extensions: ['.tsx', '.ts', '.js'] }, devtool: production ? 'source-map' : 'cheap-module-source-map', mode: production ? 'production' : 'development', optimization: { minimizer: [new TerserPlugin()] } }; };