diff --git a/@types/Canvas.ts b/@types/Canvas.ts
new file mode 100644
index 0000000..9bd9f51
--- /dev/null
+++ b/@types/Canvas.ts
@@ -0,0 +1,53 @@
+import { memo, ReactElement } from 'react';
+
+export type PanState = { x: number, y: number };
+
+export type CanvasProps = {
+ /**
+ * Since Canvas is a controlled component, the 'pan' prop defines the canvas panning
+ */
+ pan?: PanState,
+ /**
+ * Since Canvas is a controlled component, the 'onPanChange' prop is the change handler of the 'pan' prop
+ */
+ onPanChange?: (panState: PanState) => unknown,
+ /**
+ * Since Canvas is a controlled component, the 'zoom' prop defines its zoom level, aka: how much the canvas is scaling
+ */
+ zoom?: number,
+ /**
+ * Since Canvas is a controlled component, the 'onZoomChange' prop is the change handler of the 'zoom' prop
+ */
+ onZoomChange?: (zoom: number) => unknown,
+ /**
+ * Allow to zoom in/out on mouse wheel
+ */
+ zoomOnWheel?: boolean,
+ /**
+ * The maximum allowed zoom
+ */
+ maxZoom?: number,
+ /**
+ * The minimum allowed zoom
+ */
+ minZoom?: number,
+ /**
+ * Defines whether the zoom should be reset on double click
+ */
+ zoomResetOnDblClick?: boolean,
+ /**
+ * Defines whether the canvas should apply inertia when the drag is over
+ */
+ inertia?: boolean,
+ /**
+ * Displays debug info
+ */
+ debug?: boolean,
+ GridRenderer?: ReactElement,
+ ElementRenderer?: ReactElement,
+}
+
+
+declare const Canvas: (props: CanvasProps) => JSX.Element;
+
+export default memo(Canvas);
diff --git a/@types/CanvasControls.ts b/@types/CanvasControls.ts
new file mode 100644
index 0000000..964426e
--- /dev/null
+++ b/@types/CanvasControls.ts
@@ -0,0 +1,20 @@
+import { memo, ElementType } from 'react';
+import { PanState } from './Canvas';
+
+
+export type CanvasControlsProps = {
+ placement?: 'top-left' | 'top-right' | 'top-center' | 'bottom-right' | 'bottom-center' | 'bottom-left' | 'left' | 'right',
+ alignment?: 'vertical' | 'horizontal',
+ onPanChange?: (panState: PanState) => unknown,
+ onZoomChange?: (zoom: PanState) => unknown,
+ ButtonRender?: ElementType,
+ ZoomInBtnRender?: ElementType,
+ CenterBtnRender?: ElementType,
+ ZoomOutBtnRender?: ElementType,
+ ElementRender?: ElementType,
+}
+
+
+declare const CanvasControls: (props: CanvasControlsProps) => JSX.Element;
+
+export default memo(CanvasControls);
diff --git a/@types/useCanvasState.ts b/@types/useCanvasState.ts
new file mode 100644
index 0000000..e2d7281
--- /dev/null
+++ b/@types/useCanvasState.ts
@@ -0,0 +1,16 @@
+import { PanState } from './Canvas';
+
+
+export type CanvasMethods = {
+ onPanChange: (panState: PanState) => unknown,
+ onZoomChange: (zoom: number) => unknown,
+}
+
+export type CanvasStates = {
+ pan: PanState,
+ zoom: number,
+}
+
+declare const useCanvasState: (initialStates?: CanvasStates) => [CanvasStates, CanvasMethods];
+
+export default useCanvasState;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42f3bac..7c4f03e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -130,10 +130,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.5.0] - 2020-11-20
- ### Added
+### Added
+
+- First implementation of draggable canvas
+- First implementation of zoomable canvas
+
+
+## [0.5.1] - 2020-11-25
+
+### Added
- - First implementation of draggable canvas
- - First implementation of zoomable canvas
+- Added `disconnect` function exported in `useSchema` hook
## [0.5.1] - 2020-11-27
@@ -142,3 +149,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reverted changes in `0.5.0` related to draggable canvas and zoomable
canvas due to an uncaught bug. We will continue working on these features
and release them in an upcoming version. Apologies everyone!
+
+## [0.6.0] - 2020-12-12
+
+### Added
+
+- Canvas Component for panning and zooming
+- useCanvas hook
+- CanvasControl component
diff --git a/docs/Basic-usage.md b/docs/Basic-usage.md
new file mode 100644
index 0000000..51b6f54
--- /dev/null
+++ b/docs/Basic-usage.md
@@ -0,0 +1,55 @@
+To start using the library import a `Canvas` and a `Diagram` component, both are [controlled components](https://reactjs.org/docs/forms.html#controlled-components)
+so you'll need to provide a [state](https://reactjs.org/docs/faq-state.html) and a [state handler](https://reactjs.org/docs/faq-state.html#how-do-i-update-state-with-values-that-depend-on-the-current-state)
+(*beautiful-react-diagrams* exports mainly controlled components).
+
+A *Diagram* component needs to be wrapped into a *Canvas* which allows panning/zooming functionality.
+
+A *Diagram* can easily be represented by a "*schema*" (the library provides a set of pre-made utilities to define and validate schemas).
+A "*schema*" is a plain object having, at least, a "*nodes*" property defined.
+
+The "*nodes*" property must be an array of tuples (objects) described by a unique "*id*" (if not provided the library will create a unique id for the node),
+a "*content*" property (can be a React component) and a "*coordinates*" property describing the node position.
+
+Optionally a "*links*" property can be defined to define links between the nodes, similar to the "*nodes*" property it must
+be an array of valid link describing tuples, a valid link must have an "*input*" and an "*output*" property.
+
+In order to avoid unnecessary complexity the `useSchema`, `useCanvasState` hooks have been provided together with the
+ `createSchema` utility.
+
+```js
+import Diagram, { Canvas, createSchema, useSchema, useCanvasState, CanvasControls } from 'beautiful-react-diagrams';
+
+// the diagram model
+const initialSchema = createSchema({
+ nodes: [
+ { id: 'node-1', content: 'Hey Jude', coordinates: [312, 27], },
+ { id: 'node-2', content: 'Don\'t', coordinates: [330, 90], },
+ { id: 'node-3', content: 'be afraid', coordinates: [100, 320], },
+ { id: 'node-4', content: 'let me down', coordinates: [306, 332], },
+ { id: 'node-5', content: 'make it bad', coordinates: [515, 330], },
+ ],
+ links: [
+ { input: 'node-1', output: 'node-2' },
+ { input: 'node-2', output: 'node-3' },
+ { input: 'node-2', output: 'node-4' },
+ { input: 'node-2', output: 'node-5' },
+ ]
+});
+
+const DiagramExample = () => {
+ const [canvasState, handlers] = useCanvasState(); // creates canvas state
+ const [schema, { onChange }] = useSchema(initialSchema); // creates diagrams schema
+
+ return (
+
-
+
+
+
+
+
);
};
-```
+````
+
diff --git a/docs/dynamic-nodes.md b/docs/dynamic-nodes.md
index ae4ca91..5f4d935 100644
--- a/docs/dynamic-nodes.md
+++ b/docs/dynamic-nodes.md
@@ -1,5 +1,5 @@
-```jsx
-import Diagram, { createSchema, useSchema } from 'beautiful-react-diagrams';
+```js
+import Diagram, { Canvas, createSchema, useSchema, useCanvasState, CanvasControls } from 'beautiful-react-diagrams';
import { Button } from 'beautiful-react-ui';
const initialSchema = createSchema({
@@ -31,7 +31,9 @@ const CustomRender = ({ id, content, data, inputs, outputs }) => (
const UncontrolledDiagram = () => {
// create diagrams schema
const [schema, { onChange, addNode, removeNode }] = useSchema(initialSchema);
-
+ const [canvasStates, handlers] = useCanvasState(); // creates canvas state
+
+
const deleteNodeFromSchema = (id) => {
const nodeToRemove = schema.nodes.find(node => node.id === id);
removeNode(nodeToRemove);
@@ -52,12 +54,15 @@ const UncontrolledDiagram = () => {
};
addNode(nextNode);
- }
+ };
return (
Add new node
-
+
+
+
+
);
};
diff --git a/docs/links.md b/docs/links.md
deleted file mode 100644
index 3156a8b..0000000
--- a/docs/links.md
+++ /dev/null
@@ -1,32 +0,0 @@
-### Standard Links
-
-```js
-import Diagram, { useSchema, createSchema } from 'beautiful-react-diagrams';
-
-const initialSchema = createSchema({
- nodes: [
- { id: 'node-1', content: 'Node 1', coordinates: [250, 60], },
- { id: 'node-2', content: 'Node 2', coordinates: [100, 200], },
- { id: 'node-3', content: 'Node 3', coordinates: [250, 220], },
- { id: 'node-4', content: 'Node 4', coordinates: [400, 200], },
- ],
- links: [
- { input: 'node-1', output: 'node-2', label: 'Link 1', readonly: true },
- { input: 'node-1', output: 'node-3', label: 'Link 2', readonly: true },
- { input: 'node-1', output: 'node-4', label: 'Link 3', readonly: true, className: 'my-custom-link-class' },
- ]
-});
-
-const UncontrolledDiagram = () => {
- // create diagrams schema
- const [schema, { onChange }] = useSchema(initialSchema);
-
- return (
-
-
-
- );
-};
-
-
-```
diff --git a/docs/hooks.md b/docs/other-libraries.md
similarity index 100%
rename from docs/hooks.md
rename to docs/other-libraries.md
diff --git a/docs/ports.md b/docs/ports.md
deleted file mode 100644
index fe1cd96..0000000
--- a/docs/ports.md
+++ /dev/null
@@ -1,72 +0,0 @@
-```js
-import Diagram, { useSchema, createSchema } from 'beautiful-react-diagrams';
-
-const initialSchema = createSchema({
- nodes: [
- {
- id: 'node-1',
- content: 'Start',
- coordinates: [100, 150],
- outputs: [
- { id: 'port-1', alignment: 'right' },
- { id: 'port-2', alignment: 'right' },
- ],
- disableDrag: true,
- data: {
- foo: 'bar',
- count: 0,
- }
- },
- {
- id: 'node-2',
- content: 'Middle',
- coordinates: [300, 150],
- inputs: [
- { id: 'port-3', alignment: 'left' },
- { id: 'port-4', alignment: 'left' },
- ],
- outputs: [
- { id: 'port-5', alignment: 'right' },
- { id: 'port-6', alignment: 'right' },
- ],
- data: {
- bar: 'foo',
- }
- },
- {
- id: 'node-3',
- content: 'End',
- coordinates: [600, 150],
- inputs: [
- { id: 'port-7', alignment: 'left' },
- { id: 'port-8', alignment: 'left' },
- ],
- data: {
- foo: true,
- bar: false,
- some: {
- deep: {
- object: true,
- }
- },
- }
- },
- ],
- links: [
- { input: 'port-1', output: 'port-4' },
- ]
-});
-
-const UncontrolledDiagram = () => {
- // create diagrams schema
- const [schema, { onChange }] = useSchema(initialSchema);
-
- return (
-
-
-
- );
-};
-
-
-```
diff --git a/docs/setup/CustomComponentListRenderer.js b/docs/setup/CustomComponentListRenderer.js
index 39dad86..ece3133 100644
--- a/docs/setup/CustomComponentListRenderer.js
+++ b/docs/setup/CustomComponentListRenderer.js
@@ -6,9 +6,8 @@ const SidebarItem = (props) => {
return (
<>
- {visibleName === 'Hooks' &&
}
- {visibleName === 'Concepts' &&
}
+ {['Concepts', 'Dynamic nodes', 'Schema utilities', 'useCanvasState'].includes(visibleName) &&
}
>
);
};
diff --git a/docs/setup/styleguidist.config.js b/docs/setup/styleguidist.config.js
index 8e6132a..5021e9b 100644
--- a/docs/setup/styleguidist.config.js
+++ b/docs/setup/styleguidist.config.js
@@ -12,7 +12,7 @@ module.exports = {
text: 'Fork me on GitHub',
},
styleguideDir: '../../dist-ghpages',
- exampleMode: 'collapse',
+ exampleMode: 'expand',
usageMode: 'collapse',
pagePerSection: true,
sortProps: props => props,
@@ -29,23 +29,18 @@ module.exports = {
sectionDepth: 1,
},
{
- name: 'Diagram Component',
- content: '../../src/Diagram/README.md',
+ name: 'Basic Usage',
+ content: '../Basic-usage.md',
sectionDepth: 1,
},
{
- name: 'Schema',
- content: '../schema.md',
+ name: 'Links and Ports',
+ content: '../Links-Ports.md',
sectionDepth: 1,
},
{
- name: 'Linking nodes',
- content: '../links.md',
- sectionDepth: 1,
- },
- {
- name: 'Ports',
- content: '../ports.md',
+ name: 'Canvas Controls',
+ content: '../CanvasControls.md',
sectionDepth: 1,
},
{
@@ -53,16 +48,29 @@ module.exports = {
content: '../customisation.md',
sectionDepth: 1,
},
-
- { divider: true },
{
name: 'Dynamic nodes',
content: '../dynamic-nodes.md',
sectionDepth: 1,
},
{
- name: 'Hooks',
- content: '../hooks.md',
+ name: 'Schema utilities',
+ content: '../Schema-utils.md',
+ sectionDepth: 1,
+ },
+ {
+ name: 'useSchema',
+ content: '../useSchema.md',
+ sectionDepth: 1,
+ },
+ {
+ name: 'useCanvasState',
+ content: '../useCanvasState.md',
+ sectionDepth: 1,
+ },
+ {
+ name: 'Other libraries',
+ content: '../other-libraries.md',
sectionDepth: 1,
},
],
diff --git a/docs/setup/styleguidist.theme.js b/docs/setup/styleguidist.theme.js
index f4b8566..831b654 100644
--- a/docs/setup/styleguidist.theme.js
+++ b/docs/setup/styleguidist.theme.js
@@ -37,14 +37,14 @@ module.exports = {
sidebar: {
border: 0,
width: '16rem',
- background: 'white',
+ background: '#FBFAF9',
boxShadow: '0 0 20px 0 rgba(20, 20, 20, 0.1)',
},
content: {
- maxWidth: '100%',
+ maxWidth: '1024px',
},
root: {
- background: '#FBFAF9',
+ background: 'white',
},
hasSidebar: {
paddingLeft: '16rem',
diff --git a/docs/setup/styleguidist.webpack.js b/docs/setup/styleguidist.webpack.js
index 2d67519..693e793 100644
--- a/docs/setup/styleguidist.webpack.js
+++ b/docs/setup/styleguidist.webpack.js
@@ -6,25 +6,16 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const sourcePath = path.resolve(__dirname, '../..', 'src');
module.exports = () => ({
- entry: [
- `${sourcePath}/theme/index.scss`,
- `${sourcePath}/index.js`,
- ],
devtool: 'inline-source-map',
- output: {
- filename: 'beautiful-react-diagrams.dev.js',
- },
resolve: {
extensions: ['.js', '.jsx', 'scss'],
alias: { 'beautiful-react-diagrams': sourcePath },
},
devServer: {
+ contentBase: sourcePath,
open: true,
- hot: false,
- liveReload: true,
- watchContentBase: true,
+ hot: true,
},
- mode: 'development',
module: {
rules: [
{
@@ -32,21 +23,18 @@ module.exports = () => ({
exclude: /node_modules/,
use: {
loader: 'babel-loader',
+ options: {
+ presets: [
+ '@babel/preset-env',
+ '@babel/preset-react'
+ ]
+ }
},
},
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
- use: [
- MiniCssExtractPlugin.loader,
- {
- loader: 'css-loader',
- options: {
- importLoaders: 2,
- },
- },
- { loader: 'sass-loader' },
- ],
+ use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
{
test: /\.png$/,
diff --git a/docs/useCanvasState.md b/docs/useCanvasState.md
new file mode 100644
index 0000000..0b87248
--- /dev/null
+++ b/docs/useCanvasState.md
@@ -0,0 +1,21 @@
+Since *Canvas* is a [controlled components](https://reactjs.org/docs/forms.html#controlled-components), it needs to
+be provided with a "*pan*" and a "*zoom*" states, and an "*onPanChange*" and an "*onZoomChange" handlers.
+
+Being a *controlled component* allows extreme flexibility in manipulating the *Canvas* state at runtime,
+on the other hand, the operations performed on its states are quite often the same.
+
+For this reason I've summed up the most common operations in the `useCanvasState` hook.
+
+```typescript static
+type CanvasMethods = {
+ onPanChange: (panState: PanState) => unknown,
+ onZoomChange: (zoom: number) => unknown,
+}
+
+type CanvasStates = {
+ pan: PanState,
+ zoom: number,
+}
+
+declare const useCanvasState: (initialStates: CanvasStates) => [CanvasStates, CanvasMethods];
+```
diff --git a/docs/useSchema.md b/docs/useSchema.md
new file mode 100644
index 0000000..0639b2a
--- /dev/null
+++ b/docs/useSchema.md
@@ -0,0 +1,17 @@
+Since *Diagram* is a [controlled components](https://reactjs.org/docs/forms.html#controlled-components), it needs to
+be provided with a "*schema*", which represents its state, and an "*onChange*" handler.
+
+Being a *controlled component* allows extreme flexibility in manipulating the schema at runtime, on the other hand, the
+operations performed on a schema are quite often the same. For this reason I've summed up the most common operations
+in the `useSchema` hook.
+
+```typescript static
+type DiagramMethods
= {
+ onChange: (schemaChanges: DiagramSchema
) => undefined;
+ addNode: (node: Node
) => undefined;
+ removeNode: (node: Node
) => undefined;
+ connect: (inputId: string, outputId: string) => undefined;
+};
+
+declare const useSchema:
(initialSchema: DiagramSchema
) => [DiagramSchema
, DiagramMethods
];
+```
diff --git a/index.d.ts b/index.d.ts
index 5df0d34..1b936c0 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,6 +1,10 @@
-import Diagram from "./@types/Diagram";
-import useSchema from "./@types/useSchema";
-import createSchema from "./@types/createSchema";
+import Diagram from './@types/Diagram';
+import useSchema from './@types/useSchema';
+import createSchema from './@types/createSchema';
+import Canvas from './@types/Canvas';
+import CanvasControls from './@types/CanvasControls';
+import useCanvasState from './@types/useCanvasState';
+
import {
validateSchema,
validatePort,
@@ -8,7 +12,7 @@ import {
validateNodes,
validateNode,
validateLinks,
-} from "./@types/validators";
+} from './@types/validators';
export {
Diagram,
@@ -20,5 +24,8 @@ export {
validateNodes,
validateNode,
validateLinks,
+ Canvas,
+ CanvasControls,
+ useCanvasState,
};
export default Diagram;
diff --git a/package.json b/package.json
index cc8b910..34fe2d4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "beautiful-react-diagrams",
- "version": "0.5.1",
+ "version": "0.6.0",
"description": "A tiny collection of lightweight React components to build diagrams with ease",
"main": "index.js",
"module": "esm/index.js",
@@ -8,7 +8,7 @@
"scripts": {
"build": "npx del-cli dist && rollup -c",
"build-doc": "npx styleguidist build --config docs/setup/styleguidist.config.js",
- "start": "styleguidist server --config docs/setup/styleguidist.config.js",
+ "start": "npx styleguidist server --config docs/setup/styleguidist.config.js",
"lint-js": "eslint --ext .jsx,.js src/",
"lint-tests": "eslint --ext .jsx,.js tests/",
"lint-scss": "stylelint '**/*.scss'",
@@ -31,18 +31,18 @@
},
"devDependencies": {
"@babel/cli": "^7.12.1",
- "@babel/core": "^7.12.3",
+ "@babel/core": "7.12.7",
"@babel/polyfill": "^7.12.1",
- "@babel/preset-env": "^7.12.1",
+ "@babel/preset-env": "7.12.7",
"@babel/preset-react": "^7.12.1",
"@babel/register": "^7.12.1",
"@rollup/plugin-babel": "^5.2.1",
- "@rollup/plugin-node-resolve": "^9.0.0",
+ "@rollup/plugin-node-resolve": "9.0.0",
"@testing-library/react": "^11.1.0",
"@types/react": "^16.9.53",
"autoprefixer": "9.0.0",
"babel-eslint": "^10.1.0",
- "babel-loader": "^8.1.0",
+ "babel-loader": "8.2.1",
"beautiful-react-ui": "^0.56.14",
"chai": "^4.2.0",
"css-loader": "^5.0.0",
@@ -61,7 +61,7 @@
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "^1.1.1",
"mocha": "^8.2.0",
- "node-sass": "^4.14.1",
+ "node-sass": "4.14.1",
"nyc": "^15.1.0",
"postcss": "^8.1.2",
"postcss-fixes": "^2.0.1",
@@ -69,9 +69,9 @@
"postcss-preset-env": "^6.7.0",
"postcss-will-change": "3.0.0",
"postcss-will-change-transition": "^1.2.0",
- "react": "^16.14.0",
- "react-dom": "^16.12.0",
- "react-styleguidist": "^11.1.0",
+ "react": "16.14.0",
+ "react-dom": "16.14.0",
+ "react-styleguidist": "11.1.3",
"rollup": "^2.32.1",
"rollup-plugin-postcss": "^3.1.8",
"sass": "^1.27.0",
@@ -84,8 +84,7 @@
"stylelint-scss": "^3.18.0",
"typescript": "4.0.5",
"url-loader": "^4.1.1",
- "webpack": "4.44.0",
- "webpack-cli": "4.1.0"
+ "webpack": "5.6.0"
},
"dependencies": {
"beautiful-react-hooks": "^0.31.0",
@@ -96,7 +95,7 @@
"prop-types": "^15.7.2"
},
"peerDependencies": {
- "react": ">=16.14.0",
- "react-dom": ">=16.12.0"
+ "react": ">=17.0.1",
+ "react-dom": ">=17.0.1"
}
}
diff --git a/src/Context/DiagramContext.js b/src/Context/DiagramContext.js
index db2b024..2f20459 100644
--- a/src/Context/DiagramContext.js
+++ b/src/Context/DiagramContext.js
@@ -1,3 +1,3 @@
import React from 'react';
-export default React.createContext({ canvas: null, ports: null, nodes: null });
+export default React.createContext({ canvas: null, ports: null, nodes: null, panVal: { x: 0, y: 0 }, scaleVal: 1 });
diff --git a/src/Diagram/README.md b/src/Diagram/README.md
deleted file mode 100644
index 0f0afbb..0000000
--- a/src/Diagram/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-To start representing diagrams a valid model object shall be provided to the component via the `schema` prop.
-A valid model is a plain object having a `nodes` property set.
-The `nodes` property must be an array of tuples (objects) described by a unique `id` (it must be unique ),
-a `content` property (can be a React component) and a `coordinates` property describing the node position.
-Optionally a `links` property can be set describing links between the nodes, similar to the `nodes` property it must
-be an array of valid link describing tuples, a valid link must have an `input` and an `output` property.
-
-```js
-import Diagram, { createSchema, useSchema } from 'beautiful-react-diagrams';
-
-// the diagram model
-const initialSchema = createSchema({
- nodes: [
- { id: 'node-1', content: 'Node 1', coordinates: [250, 60], },
- { id: 'node-2', content: 'Node 2', coordinates: [100, 200], },
- { id: 'node-3', content: 'Node 3', coordinates: [250, 220], },
- { id: 'node-4', content: 'Node 4', coordinates: [400, 200], },
- ],
- links: [
- { input: 'node-1', output: 'node-2' },
- { input: 'node-1', output: 'node-3' },
- { input: 'node-1', output: 'node-4' },
- ]
-});
-
-const UncontrolledDiagram = () => {
- // create diagrams schema
- const [schema, { onChange }] = useSchema(initialSchema);
-
- return (
-
-
-
- );
-};
-
-
-```
-
diff --git a/src/components/Canvas/BackgroundGrid.js b/src/components/Canvas/BackgroundGrid.js
new file mode 100644
index 0000000..aaa6ae2
--- /dev/null
+++ b/src/components/Canvas/BackgroundGrid.js
@@ -0,0 +1,48 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+
+const parallaxRatio = 1.25;
+const calcCoordinates = (x, y) => ([x * parallaxRatio, y * parallaxRatio]);
+const calcTransformation = (x, y, scale) => (`scale(${scale}) translate(${x}, ${y})`);
+
+/**
+ * TODO: document me
+ */
+const BackgroundGrid = ({ translateX, translateY, scale, svgPatternColor, svgPatternOpacity }) => {
+ const [x, y] = useMemo(() => calcCoordinates(translateX, translateY), [translateX, translateY]);
+ const transformation = useMemo(() => calcTransformation(x, y, scale), [x, y, scale]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+BackgroundGrid.propTypes = {
+ svgPatternColor: PropTypes.string,
+ svgPatternOpacity: PropTypes.number,
+ translateX: PropTypes.number,
+ translateY: PropTypes.number,
+ scale: PropTypes.number,
+};
+
+BackgroundGrid.defaultProps = {
+ svgPatternColor: 'black',
+ svgPatternOpacity: 0.5,
+ translateX: 0,
+ translateY: 0,
+ scale: 1,
+};
+
+export default React.memo(BackgroundGrid);
diff --git a/src/components/Canvas/Canvas.js b/src/components/Canvas/Canvas.js
new file mode 100644
index 0000000..4299410
--- /dev/null
+++ b/src/components/Canvas/Canvas.js
@@ -0,0 +1,108 @@
+import React, { useMemo, useRef } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import useCanvasPanHandlers from './useCanvasPanHandlers';
+import useCanvasZoomHandlers from './useCanvasZoomHandlers';
+import BackgroundGrid from './BackgroundGrid';
+import { noop } from '../../shared/Constants';
+import { filterControlsOut, enrichControls } from './childrenUtils';
+
+import './canvas.scss';
+
+const calcTransformation = (scale = 1, { x = 0, y = 0 }) => ({
+ transform: `translate(${x}px, ${y}px) translateZ(0) scale(${scale})`,
+});
+
+/**
+ * @TODO: Document this component
+ */
+const Canvas = (props) => {
+ const {
+ pan, onPanChange, zoom, onZoomChange, maxZoom, minZoom, zoomOnWheel, inertia, zoomResetOnDblClick,
+ ElementRenderer, GridRenderer, debug, className, children, ...rest
+ } = props;
+ const elRef = useRef();
+ const classList = useMemo(() => classNames('bi bi-diagram bi-diagram-canvas', className), [className]);
+ const style = useMemo(() => calcTransformation(zoom, pan), [zoom, pan.x, pan.y]);
+ const startPan = useCanvasPanHandlers({ pan, onPanChange, inertia });
+
+ useCanvasZoomHandlers(elRef, { onZoomChange, maxZoom, minZoom, zoomOnWheel, zoomResetOnDblClick });
+
+ return (
+
+
+
+ {filterControlsOut(children, pan, zoom)}
+
+ {debug && (
+
+
{`Pan: ${pan.x}, ${pan.y}`}
+
{`Scale: ${zoom}`}
+
+ )}
+ {enrichControls(children, { onPanChange, onZoomChange, minZoom, maxZoom })}
+
+ );
+};
+
+Canvas.propTypes = {
+ /**
+ * Since Canvas is a controlled component, the 'pan' prop defines the canvas panning
+ */
+ pan: PropTypes.exact({ x: PropTypes.number, y: PropTypes.number }),
+ /**
+ * Since Canvas is a controlled component, the 'onPanChange' prop is the change handler of the 'pan' prop
+ */
+ onPanChange: PropTypes.func,
+ /**
+ * Since Canvas is a controlled component, the 'zoom' prop defines its zoom level, aka: how much the canvas is scaling
+ */
+ zoom: PropTypes.number,
+ /**
+ * Since Canvas is a controlled component, the 'onZoomChange' prop is the change handler of the 'zoom' prop
+ */
+ onZoomChange: PropTypes.func,
+ /**
+ * Allow to zoom in/out on mouse wheel
+ */
+ zoomOnWheel: PropTypes.bool,
+ /**
+ * The maximum allowed zoom
+ */
+ maxZoom: PropTypes.number,
+ /**
+ * The minimum allowed zoom
+ */
+ minZoom: PropTypes.number,
+ /**
+ * Defines whether the zoom should be reset on double click
+ */
+ zoomResetOnDblClick: PropTypes.bool,
+ /**
+ * Defines whether the canvas should apply inertia when the drag is over
+ */
+ inertia: PropTypes.bool,
+ /**
+ * Displays debug info
+ */
+ debug: PropTypes.bool,
+ GridRenderer: PropTypes.elementType,
+ ElementRenderer: PropTypes.elementType,
+};
+
+Canvas.defaultProps = {
+ pan: { x: 0, y: 0 },
+ onPanChange: noop,
+ zoom: 1,
+ onZoomChange: noop,
+ zoomOnWheel: true,
+ maxZoom: 2,
+ minZoom: 0.5,
+ zoomResetOnDblClick: true,
+ inertia: true,
+ debug: false,
+ GridRenderer: BackgroundGrid,
+ ElementRenderer: 'div',
+};
+
+export default React.memo(Canvas);
diff --git a/src/components/Canvas/canvas.scss b/src/components/Canvas/canvas.scss
new file mode 100644
index 0000000..dd75e32
--- /dev/null
+++ b/src/components/Canvas/canvas.scss
@@ -0,0 +1,39 @@
+.bi.bi-diagram.bi-diagram-canvas {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ background: #fbfaf9;
+ border: 0.07rem solid rgba(0, 0, 0, 0.2);
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
+
+ &:active {
+ cursor: grabbing;
+ }
+
+ .bi-canvas-content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ .bi-canvas-debugger {
+ max-width: 24rem;
+ height: 3rem;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.1);
+
+ p {
+ margin: 0;
+ }
+ }
+}
diff --git a/src/components/Canvas/childrenUtils.js b/src/components/Canvas/childrenUtils.js
new file mode 100644
index 0000000..1d1c6cc
--- /dev/null
+++ b/src/components/Canvas/childrenUtils.js
@@ -0,0 +1,20 @@
+import React, { Children, cloneElement } from 'react';
+import CanvasControls from '../CanvasControls';
+
+// todo: document this method
+export const filterControlsOut = (children, pan, scale) => Children.map(children, (C) => (C.type !== CanvasControls
+ ? React.cloneElement(C, { pan, scale })
+ : null));
+
+// todo: document this method
+export const enrichControls = (children, props) => Children.map(children, (C) => {
+ if (C.type === CanvasControls) {
+ return cloneElement(C, {
+ onPanChange: C.props.onPanChange || props.onPanChange,
+ onZoomChange: C.props.onZoomChange || props.onZoomChange,
+ minZoom: C.props.minZoom || props.minZoom,
+ maxZoom: C.props.maxZoom || props.maxZoom,
+ });
+ }
+ return null;
+});
diff --git a/src/components/Canvas/index.js b/src/components/Canvas/index.js
new file mode 100644
index 0000000..dd22256
--- /dev/null
+++ b/src/components/Canvas/index.js
@@ -0,0 +1 @@
+export { default } from './Canvas';
diff --git a/src/components/Canvas/useCanvasPanHandlers.js b/src/components/Canvas/useCanvasPanHandlers.js
new file mode 100644
index 0000000..79e0756
--- /dev/null
+++ b/src/components/Canvas/useCanvasPanHandlers.js
@@ -0,0 +1,73 @@
+import { useCallback, useRef } from 'react';
+import { Events, isTouch } from '../../shared/Constants';
+
+const friction = 0.8; // TODO: document this stuff
+const getMouseEventPoint = (e) => ({ x: e.pageX, y: e.pageY });
+const getTouchEventPoint = (e) => getMouseEventPoint(e.changedTouches[0]);
+const getEventPoint = isTouch ? getTouchEventPoint : getMouseEventPoint;
+const calculateDelta = (current, last) => ({ x: last.x - current.x, y: last.y - current.y });
+const applyInertia = (value) => (Math.abs(value) >= 0.5 ? Math.trunc(value * friction) : 0);
+
+/**
+ * TODO: document this thing
+ * Inspired by this article:
+ * https://jclem.net/posts/pan-zoom-canvas-react?utm_campaign=building-a-s--zoomable-canvasdi
+ */
+const useCanvasPanHandlers = ({ pan, onPanChange, inertia }) => {
+ const lastPointRef = useRef(pan);
+ const deltaRef = useRef({ x: null, y: null });
+
+ // TODO: document this callback
+ const performPan = useCallback((event) => {
+ if (onPanChange) {
+ const lastPoint = { ...lastPointRef.current };
+ const point = getEventPoint(event);
+ lastPointRef.current = point;
+ onPanChange(({ x, y }) => {
+ const delta = calculateDelta(lastPoint, point);
+ deltaRef.current = { ...delta };
+
+ return { x: x + delta.x, y: y + delta.y };
+ });
+ }
+ }, []);
+
+ // TODO: document this callback
+ const performInertia = useCallback(() => {
+ if (inertia) {
+ onPanChange(({ x, y }) => ({ x: x + deltaRef.current.x, y: y + deltaRef.current.y }));
+
+ deltaRef.current.x = applyInertia(deltaRef.current.x);
+ deltaRef.current.y = applyInertia(deltaRef.current.y);
+
+ if (Math.abs(deltaRef.current.x) > 0 || Math.abs(deltaRef.current.y) > 0) {
+ requestAnimationFrame(performInertia);
+ }
+ }
+ }, [inertia, deltaRef.current.x, deltaRef.current.y]);
+
+ // TODO: document this callback
+ const endPan = useCallback(() => {
+ if (onPanChange) {
+ document.removeEventListener(Events.MOUSE_MOVE, performPan);
+ document.removeEventListener(Events.MOUSE_END, endPan);
+
+ if (inertia) {
+ requestAnimationFrame(performInertia);
+ }
+ }
+ }, [performPan, inertia, onPanChange]);
+
+ // TODO: document this callback
+ const onPanStart = useCallback((event) => {
+ if (onPanChange) {
+ document.addEventListener(Events.MOUSE_MOVE, performPan);
+ document.addEventListener(Events.MOUSE_END, endPan);
+ lastPointRef.current = getEventPoint(event);
+ }
+ }, [onPanChange, performPan, endPan]);
+
+ return onPanStart;
+};
+
+export default useCanvasPanHandlers;
diff --git a/src/components/Canvas/useCanvasZoomHandlers.js b/src/components/Canvas/useCanvasZoomHandlers.js
new file mode 100644
index 0000000..afe8c21
--- /dev/null
+++ b/src/components/Canvas/useCanvasZoomHandlers.js
@@ -0,0 +1,54 @@
+import { useEffect, useCallback } from 'react';
+import { Events } from '../../shared/Constants';
+
+// TODO: move to the hooks library
+const useEvent = (ref, event, callback, options) => {
+ useEffect(() => {
+ if (ref.current) {
+ ref.current.addEventListener(event, callback, options);
+ }
+
+ return () => {
+ if (ref.current) {
+ ref.current.removeEventListener(event, callback, options);
+ }
+ };
+ }, [ref.current]);
+};
+
+const defaultOptions = { zoom: 1, maxZoom: 5, minZoom: 0.4 };
+const wheelOffset = 0.01; // TODO: document this
+
+/**
+ * TODO: document this thing
+ * inspired by: https://jclem.net/posts/pan-zoom-canvas-react?utm_campaign=building-a-pannable--zoomable-canvasdi
+ */
+const useCanvasZoomHandlers = (ref, options = defaultOptions) => {
+ const { onZoomChange, maxZoom, minZoom, zoomOnWheel, zoomResetOnDblClick } = options;
+
+ const scaleOnWheel = useCallback((event) => {
+ if (onZoomChange && zoomOnWheel) {
+ event.preventDefault(); // FIXME: double check the bubbling of this event you know nothing about
+
+ onZoomChange((currentScale) => {
+ if (event.deltaY > 0) {
+ return (currentScale + wheelOffset < maxZoom) ? (currentScale + wheelOffset) : maxZoom;
+ }
+
+ return (currentScale - wheelOffset > minZoom) ? (currentScale - wheelOffset) : minZoom;
+ });
+ }
+ }, [onZoomChange, maxZoom, minZoom]);
+
+ const resetZoom = useCallback((event) => {
+ if (onZoomChange && zoomResetOnDblClick) {
+ event.preventDefault();
+ onZoomChange(1);
+ }
+ }, []);
+
+ useEvent(ref, Events.WHEEL, scaleOnWheel, { passive: false });
+ useEvent(ref, Events.DOUBLE_CLICK, resetZoom, { passive: false });
+};
+
+export default useCanvasZoomHandlers;
diff --git a/src/components/CanvasControls/CanvasControls.js b/src/components/CanvasControls/CanvasControls.js
new file mode 100644
index 0000000..d5da6bb
--- /dev/null
+++ b/src/components/CanvasControls/CanvasControls.js
@@ -0,0 +1,73 @@
+import React, { useMemo, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import PlusIcon from './IconPlus';
+import MinusIcon from './IconMinus';
+import CenterIcon from './IconCenter';
+import { noop } from '../../shared/Constants';
+
+import './canvas-controls.scss';
+
+/**
+ * TODO: document this thing
+ * @param props
+ * @returns {*}
+ * @constructor
+ */
+const CanvasControls = (props) => {
+ const {
+ placement, alignment, onPanChange, onZoomChange, className,
+ ElementRender, ButtonRender, ZoomInBtnRender, ZoomOutBtnRender, CenterBtnRender,
+ } = props;
+ const classList = useMemo(() => (
+ classNames('bi bi-diagram-ctrls', `bi-diagram-ctrls-${placement}`, `bi-diagram-ctrls-${alignment}`, className)
+ ), [placement, className, alignment]);
+
+ const zoomInHandler = useCallback(() => {
+ onZoomChange((currentZoom) => (currentZoom + 0.25));
+ }, [onZoomChange]);
+
+ const zoomOutHandler = useCallback(() => {
+ onZoomChange((currentZoom) => (currentZoom - 0.25));
+ }, [onZoomChange]);
+
+ const resetHandler = useCallback(() => {
+ onPanChange({ x: 0, y: 0 });
+ onZoomChange(1);
+ }, [onZoomChange, onPanChange]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+CanvasControls.propTypes = {
+ // eslint-disable-next-line max-len
+ placement: PropTypes.oneOf(['top-left', 'top-right', 'top-center', 'bottom-right', 'bottom-center', 'bottom-left', 'left', 'right']),
+ alignment: PropTypes.oneOf(['vertical', 'horizontal']),
+ onPanChange: PropTypes.func,
+ onZoomChange: PropTypes.func,
+ ButtonRender: PropTypes.elementType,
+ ZoomInBtnRender: PropTypes.elementType,
+ CenterBtnRender: PropTypes.elementType,
+ ZoomOutBtnRender: PropTypes.elementType,
+ ElementRender: PropTypes.elementType,
+};
+
+CanvasControls.defaultProps = {
+ placement: 'bottom-left',
+ alignment: 'vertical',
+ onPanChange: noop,
+ onZoomChange: noop,
+ ButtonRender: 'button',
+ ZoomInBtnRender: PlusIcon,
+ CenterBtnRender: CenterIcon,
+ ZoomOutBtnRender: MinusIcon,
+ ElementRender: 'nav',
+};
+
+export default React.memo(CanvasControls);
diff --git a/src/components/CanvasControls/IconCenter.js b/src/components/CanvasControls/IconCenter.js
new file mode 100644
index 0000000..8ed33d3
--- /dev/null
+++ b/src/components/CanvasControls/IconCenter.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+/**
+ * // TODO: document this
+ * @returns {*}
+ * @constructor
+ */
+const IconCenter = () => (
+
+
+
+
+
+
+
+
+
+);
+
+export default React.memo(IconCenter);
diff --git a/src/components/CanvasControls/IconMinus.js b/src/components/CanvasControls/IconMinus.js
new file mode 100644
index 0000000..66a6f13
--- /dev/null
+++ b/src/components/CanvasControls/IconMinus.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+/**
+ * // TODO: document this
+ * @returns {*}
+ * @constructor
+ */
+const IconMinus = () => (
+
+ {/* eslint-disable-next-line max-len */}
+
+
+);
+
+export default React.memo(IconMinus);
diff --git a/src/components/CanvasControls/IconPlus.js b/src/components/CanvasControls/IconPlus.js
new file mode 100644
index 0000000..dfea926
--- /dev/null
+++ b/src/components/CanvasControls/IconPlus.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+/**
+ * // TODO: document this
+ * @returns {*}
+ * @constructor
+ */
+const IconPlus = () => (
+
+ {/* eslint-disable-next-line max-len */}
+
+
+);
+
+export default React.memo(IconPlus);
diff --git a/src/components/CanvasControls/canvas-controls.scss b/src/components/CanvasControls/canvas-controls.scss
new file mode 100644
index 0000000..eebab77
--- /dev/null
+++ b/src/components/CanvasControls/canvas-controls.scss
@@ -0,0 +1,71 @@
+.bi.bi-diagram-ctrls {
+ box-sizing: border-box;
+ position: absolute;
+ padding: 0.75rem;
+ display: flex;
+
+ .bid-ctrls-btn {
+ width: 1rem;
+ height: 1rem;
+ padding: 0.3rem;
+ background: white;
+ border: 0.07rem solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0.12rem 0.24rem 1rem rgba(0, 0, 0, 0.1);
+ box-sizing: content-box;
+ cursor: pointer;
+ }
+
+ // Alignments
+ &.bi-diagram-ctrls-vertical {
+ flex-direction: column;
+ }
+
+ &.bi-diagram-ctrls-horizontal {
+ flex-direction: row;
+ }
+
+ // Placements
+ &.bi-diagram-ctrls-left {
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ &.bi-diagram-ctrls-right {
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ &.bi-diagram-ctrls-top-left {
+ top: 0;
+ left: 0;
+ }
+
+ &.bi-diagram-ctrls-top-right {
+ top: 0;
+ right: 0;
+ }
+
+ &.bi-diagram-ctrls-top-center {
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &.bi-diagram-ctrls-bottom-right {
+ bottom: 0;
+ right: 0;
+ }
+
+ &.bi-diagram-ctrls-bottom-center {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &.bi-diagram-ctrls-bottom-left {
+ bottom: 0;
+ left: 0;
+ }
+}
diff --git a/src/components/CanvasControls/index.js b/src/components/CanvasControls/index.js
new file mode 100644
index 0000000..26a0d48
--- /dev/null
+++ b/src/components/CanvasControls/index.js
@@ -0,0 +1 @@
+export { default } from './CanvasControls';
diff --git a/src/Diagram/Diagram.js b/src/components/Diagram/Diagram.js
similarity index 90%
rename from src/Diagram/Diagram.js
rename to src/components/Diagram/Diagram.js
index 68c46a6..7fe5e60 100644
--- a/src/Diagram/Diagram.js
+++ b/src/components/Diagram/Diagram.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import DiagramCanvas from './DiagramCanvas/DiagramCanvas';
import NodesCanvas from './NodesCanvas/NodesCanvas';
import LinksCanvas from './LinksCanvas/LinksCanvas';
-import { SchemaType } from '../shared/Types';
+import { SchemaType } from '../../shared/Types';
import './diagram.scss';
@@ -14,7 +14,7 @@ import './diagram.scss';
* with the user.
*/
const Diagram = (props) => {
- const { schema, onChange, ...rest } = props;
+ const { schema, onChange, pan, scale, ...rest } = props;
const [segment, setSegment] = useState();
const { current: portRefs } = useRef({}); // keeps the port elements references
const { current: nodeRefs } = useRef({}); // keeps the node elements references
@@ -71,7 +71,7 @@ const Diagram = (props) => {
};
return (
-
+
{
- const { children, portRefs, nodeRefs, className, ...rest } = props;
- const [bbox, setBoundingBox] = useState(null);
+ const { children, portRefs, nodeRefs, pan, scale, className, ...rest } = props;
+ const [bbox, setBoundingBox] = useState();
const canvasRef = useRef();
const classList = classNames('bi bi-diagram', className);
// calculate the given element bounding box and save it into the bbox state
- const calculateBBox = (el) => {
- if (el) {
- const nextBBox = el.getBoundingClientRect();
+ const calculateBBox = () => {
+ if (canvasRef.current) {
+ const nextBBox = canvasRef.current.getBoundingClientRect();
if (!isEqual(nextBBox, bbox)) {
setBoundingBox(nextBBox);
}
@@ -28,15 +28,16 @@ const DiagramCanvas = (props) => {
// when the canvas is ready and placed within the DOM, save its bounding box to be provided down
// to children component as a context value for future calculations.
- useEffect(() => calculateBBox(canvasRef.current), [canvasRef.current]);
+ useEffect(calculateBBox, [canvasRef.current]);
// same on window scroll and resize
- useWindowScroll(() => calculateBBox(canvasRef.current));
- useWindowResize(() => calculateBBox(canvasRef.current));
+ useWindowScroll(calculateBBox);
+ useWindowResize(calculateBBox);
return (
-
-
+
+ {/* eslint-disable-next-line max-len */}
+
{children}
@@ -48,12 +49,16 @@ DiagramCanvas.propTypes = {
portRefs: PropTypes.shape({}),
nodeRefs: PropTypes.shape({}),
className: PropTypes.string,
+ pan: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }),
+ scale: PropTypes.number,
};
DiagramCanvas.defaultProps = {
portRefs: {},
nodeRefs: {},
className: '',
+ pan: { x: 0, y: 0 },
+ scale: 1,
};
export default React.memo(DiagramCanvas);
diff --git a/src/Diagram/DiagramNode/DiagramNode.js b/src/components/Diagram/DiagramNode/DiagramNode.js
similarity index 94%
rename from src/Diagram/DiagramNode/DiagramNode.js
rename to src/components/Diagram/DiagramNode/DiagramNode.js
index fb5bd51..6baf2b5 100644
--- a/src/Diagram/DiagramNode/DiagramNode.js
+++ b/src/components/Diagram/DiagramNode/DiagramNode.js
@@ -2,11 +2,11 @@ import React, { useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import getDiagramNodeStyle from './getDiagramNodeStyle';
-import { usePortRegistration, useNodeRegistration } from '../../shared/internal_hooks/useContextRegistration';
-import { PortType } from '../../shared/Types';
+import { usePortRegistration, useNodeRegistration } from '../../../shared/internal_hooks/useContextRegistration';
+import { PortType } from '../../../shared/Types';
import portGenerator from './portGenerator';
-import useDrag from '../../shared/internal_hooks/useDrag';
-import useNodeUnregistration from '../../shared/internal_hooks/useNodeUnregistration';
+import useDrag from '../../../shared/internal_hooks/useDrag';
+import useNodeUnregistration from '../../../shared/internal_hooks/useNodeUnregistration';
/**
* A Diagram Node component displays a single diagram node, handles the drag n drop business logic and fires the
@@ -24,8 +24,9 @@ const DiagramNode = (props) => {
if (!disableDrag) {
// when drag starts, save the starting coordinates into the `dragStartPoint` ref
- onDragStart(() => {
+ onDragStart((event) => {
dragStartPoint.current = coordinates;
+ event.stopPropagation();
});
// whilst dragging calculates the next coordinates and perform the `onPositionChange` callback
diff --git a/src/Diagram/DiagramNode/getDiagramNodeStyle.js b/src/components/Diagram/DiagramNode/getDiagramNodeStyle.js
similarity index 100%
rename from src/Diagram/DiagramNode/getDiagramNodeStyle.js
rename to src/components/Diagram/DiagramNode/getDiagramNodeStyle.js
diff --git a/src/Diagram/DiagramNode/portGenerator.js b/src/components/Diagram/DiagramNode/portGenerator.js
similarity index 100%
rename from src/Diagram/DiagramNode/portGenerator.js
rename to src/components/Diagram/DiagramNode/portGenerator.js
diff --git a/src/Diagram/Link/Link.js b/src/components/Diagram/Link/Link.js
similarity index 70%
rename from src/Diagram/Link/Link.js
rename to src/components/Diagram/Link/Link.js
index e31a1f4..565794e 100644
--- a/src/Diagram/Link/Link.js
+++ b/src/components/Diagram/Link/Link.js
@@ -1,22 +1,22 @@
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { LinkType, NodeType, PortType } from '../../shared/Types';
-import usePortRefs from '../../shared/internal_hooks/usePortRefs';
-import useCanvas from '../../shared/internal_hooks/useCanvas';
+import { LinkType, NodeType, PortType } from '../../../shared/Types';
+import usePortRefs from '../../../shared/internal_hooks/usePortRefs';
+import useCanvas from '../../../shared/internal_hooks/useCanvas';
import getCoords from './getEntityCoordinates';
-import makeSvgPath from '../../shared/functions/makeSvgPath';
-import getPathMidpoint from '../../shared/functions/getPathMidpoint';
-import useNodeRefs from '../../shared/internal_hooks/useNodeRefs';
+import makeSvgPath from '../../../shared/functions/makeSvgPath';
+import getPathMidpoint from '../../../shared/functions/getPathMidpoint';
+import useNodeRefs from '../../../shared/internal_hooks/useNodeRefs';
import LinkLabel from './LinkLabel';
// local hook, returns portRefs & nodeRefs
const useContextRefs = () => {
- const canvas = useCanvas();
+ const { canvas, panVal, scaleVal } = useCanvas();
const portRefs = usePortRefs();
const nodeRefs = useNodeRefs();
- return { canvas, nodeRefs, portRefs };
+ return { canvas, panVal, scaleVal, nodeRefs, portRefs };
};
/**
@@ -26,11 +26,12 @@ const Link = (props) => {
const { input, output, link, onDelete } = props;
const pathRef = useRef();
const [labelPosition, setLabelPosition] = useState();
- const { canvas, portRefs, nodeRefs } = useContextRefs();
- const inputPoint = useMemo(() => getCoords(input, portRefs, nodeRefs, canvas), [input, portRefs, nodeRefs, canvas]);
+ const { canvas, panVal, scaleVal, portRefs, nodeRefs } = useContextRefs();
+ // eslint-disable-next-line max-len
+ const inputPoint = useMemo(() => getCoords(input, portRefs, nodeRefs, canvas, panVal), [input, portRefs, nodeRefs, canvas, panVal]);
/* eslint-disable max-len */
const classList = useMemo(() => classNames('bi-diagram-link', { 'readonly-link': link.readonly }, link.className), [link.readonly, link.className]);
- const outputPoint = useMemo(() => getCoords(output, portRefs, nodeRefs, canvas), [output, portRefs, nodeRefs, canvas]);
+ const outputPoint = useMemo(() => getCoords(output, portRefs, nodeRefs, canvas, panVal), [output, portRefs, nodeRefs, canvas, panVal]);
/* eslint-enable max-len */
const pathOptions = {
type: (input.type === 'port' || output.type === 'port') ? 'bezier' : 'curve',
@@ -54,8 +55,13 @@ const Link = (props) => {
}
}, [link.readonly, onDelete]);
+ /**
+ * for nodes with ports and links it's required to recalculate the link scale based on canvas scale
+ */
+ const nextScale = Object.keys(portRefs).length > 0 ? 1 / scaleVal : 1;
+
return (
-
+
{!link.readonly && ( )}
{link.label && labelPosition && ( )}
diff --git a/src/Diagram/Link/LinkLabel.js b/src/components/Diagram/Link/LinkLabel.js
similarity index 100%
rename from src/Diagram/Link/LinkLabel.js
rename to src/components/Diagram/Link/LinkLabel.js
diff --git a/src/Diagram/Link/getEntityCoordinates.js b/src/components/Diagram/Link/getEntityCoordinates.js
similarity index 68%
rename from src/Diagram/Link/getEntityCoordinates.js
rename to src/components/Diagram/Link/getEntityCoordinates.js
index 8b3e84c..e91ffee 100644
--- a/src/Diagram/Link/getEntityCoordinates.js
+++ b/src/components/Diagram/Link/getEntityCoordinates.js
@@ -1,20 +1,25 @@
-import getRelativePoint from '../../shared/functions/getRelativePoint';
+import getRelativePoint from '../../../shared/functions/getRelativePoint';
/**
* Return the coordinates of a given entity (node or port)
*/
-const getEntityCoordinates = (entity, portRefs, nodeRefs, canvas) => {
+// eslint-disable-next-line max-len
+const getEntityCoordinates = (entity, portRefs, nodeRefs, canvas = { x: 0, y: 0 }, pan = { x: 0, y: 0 }) => {
if (entity && entity.type === 'node' && nodeRefs[entity.entity.id]) {
const nodeEl = nodeRefs[entity.entity.id];
const bbox = nodeEl.getBoundingClientRect();
+
return [entity.entity.coordinates[0] + (bbox.width / 2), entity.entity.coordinates[1] + (bbox.height / 2)];
}
if (portRefs && portRefs[entity.entity.id]) {
+ const nextX = canvas.x + pan.x;
+ const nextY = canvas.y + pan.y;
+ const nextCanvas = [nextX, nextY];
const portEl = portRefs[entity.entity.id];
const bbox = portEl.getBoundingClientRect();
- return getRelativePoint([bbox.x + (bbox.width / 2), bbox.y + (bbox.height / 2)], [canvas.x, canvas.y]);
+ return getRelativePoint([bbox.x + (bbox.width / 2), bbox.y + (bbox.height / 2)], [nextCanvas[0], nextCanvas[1]]);
}
return undefined;
diff --git a/src/Diagram/LinksCanvas/LinksCanvas.js b/src/components/Diagram/LinksCanvas/LinksCanvas.js
similarity index 95%
rename from src/Diagram/LinksCanvas/LinksCanvas.js
rename to src/components/Diagram/LinksCanvas/LinksCanvas.js
index 503e343..f4208b4 100644
--- a/src/Diagram/LinksCanvas/LinksCanvas.js
+++ b/src/components/Diagram/LinksCanvas/LinksCanvas.js
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import DiagramLink from '../Link/Link';
import Segment from '../Segment/Segment';
-import { LinkType, NodeType, PortAlignment } from '../../shared/Types';
+import { LinkType, NodeType, PortAlignment } from '../../../shared/Types';
import findInvolvedEntity from './findInvolvedEntity';
import removeLinkFromArray from './removeLinkFromArray';
diff --git a/src/Diagram/LinksCanvas/findInvolvedEntity.js b/src/components/Diagram/LinksCanvas/findInvolvedEntity.js
similarity index 100%
rename from src/Diagram/LinksCanvas/findInvolvedEntity.js
rename to src/components/Diagram/LinksCanvas/findInvolvedEntity.js
diff --git a/src/Diagram/LinksCanvas/removeLinkFromArray.js b/src/components/Diagram/LinksCanvas/removeLinkFromArray.js
similarity index 100%
rename from src/Diagram/LinksCanvas/removeLinkFromArray.js
rename to src/components/Diagram/LinksCanvas/removeLinkFromArray.js
diff --git a/src/Diagram/NodesCanvas/NodesCanvas.js b/src/components/Diagram/NodesCanvas/NodesCanvas.js
similarity index 97%
rename from src/Diagram/NodesCanvas/NodesCanvas.js
rename to src/components/Diagram/NodesCanvas/NodesCanvas.js
index 3bcb6ce..90cdd26 100644
--- a/src/Diagram/NodesCanvas/NodesCanvas.js
+++ b/src/components/Diagram/NodesCanvas/NodesCanvas.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { NodeType } from '../../shared/Types';
+import { NodeType } from '../../../shared/Types';
import DiagramNode from '../DiagramNode/DiagramNode';
import updateNodeCoordinates from './updateNodeCoordinates';
diff --git a/src/Diagram/NodesCanvas/updateNodeCoordinates.js b/src/components/Diagram/NodesCanvas/updateNodeCoordinates.js
similarity index 100%
rename from src/Diagram/NodesCanvas/updateNodeCoordinates.js
rename to src/components/Diagram/NodesCanvas/updateNodeCoordinates.js
diff --git a/src/Diagram/Port/Port.js b/src/components/Diagram/Port/Port.js
similarity index 83%
rename from src/Diagram/Port/Port.js
rename to src/components/Diagram/Port/Port.js
index 9bba74e..d8cf174 100644
--- a/src/Diagram/Port/Port.js
+++ b/src/components/Diagram/Port/Port.js
@@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
-import useDrag from '../../shared/internal_hooks/useDrag';
-import useCanvas from '../../shared/internal_hooks/useCanvas';
-import getRelativePoint from '../../shared/functions/getRelativePoint';
+import useDrag from '../../../shared/internal_hooks/useDrag';
+import useCanvas from '../../../shared/internal_hooks/useCanvas';
+import getRelativePoint from '../../../shared/functions/getRelativePoint';
/**
* Port
@@ -12,15 +12,15 @@ import getRelativePoint from '../../shared/functions/getRelativePoint';
*/
const Port = (props) => {
const { id, canLink, alignment, onDragNewSegment, onSegmentFail, onSegmentConnect, onMount, type, ...rest } = props;
- const canvas = useCanvas();
+ const { canvas, panVal } = useCanvas();
const { ref, onDrag, onDragEnd } = useDrag();
onDrag((event, info) => {
if (onDragNewSegment) {
event.stopImmediatePropagation();
event.stopPropagation();
- const from = getRelativePoint(info.start, [canvas.x, canvas.y]);
- const to = getRelativePoint([event.clientX, event.clientY], [canvas.x, canvas.y]);
+ const from = getRelativePoint(info.start, [canvas.x + panVal.x, canvas.y + panVal.y]);
+ const to = getRelativePoint([event.clientX, event.clientY], [canvas.x + panVal.x, canvas.y + panVal.y]);
onDragNewSegment(id, from, to, alignment);
}
diff --git a/src/Diagram/Segment/Segment.js b/src/components/Diagram/Segment/Segment.js
similarity index 66%
rename from src/Diagram/Segment/Segment.js
rename to src/components/Diagram/Segment/Segment.js
index f274309..fdea853 100644
--- a/src/Diagram/Segment/Segment.js
+++ b/src/components/Diagram/Segment/Segment.js
@@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
-import { PortAlignment } from '../../shared/Types';
-import makeSvgPath from '../../shared/functions/makeSvgPath';
+import { PortAlignment } from '../../../shared/Types';
+import makeSvgPath from '../../../shared/functions/makeSvgPath';
+import useCanvas from '../../../shared/internal_hooks/useCanvas';
/**
* Segment
@@ -10,9 +11,11 @@ const Segment = (props) => {
const { from, to, alignment } = props;
const pathOptions = { type: 'bezier', inputAlignment: alignment };
const path = useMemo(() => makeSvgPath(from, to, pathOptions), [from, to, alignment]);
+ const { scaleVal } = useCanvas();
+ const nextScale = 1 / scaleVal;
return (
-
+
diff --git a/src/Diagram/diagram.scss b/src/components/Diagram/diagram.scss
similarity index 94%
rename from src/Diagram/diagram.scss
rename to src/components/Diagram/diagram.scss
index ffe7fa0..be1782b 100644
--- a/src/Diagram/diagram.scss
+++ b/src/components/Diagram/diagram.scss
@@ -2,13 +2,13 @@
box-sizing: border-box;
width: 100%;
height: 100%;
- border: 0.07rem solid #dae1e7;
- border-radius: 0.25rem;
- box-shadow: 0 0.8rem 1rem -0.2rem rgba(0, 0, 0, 0.1), 0 0.25rem 0.5rem -0.02rem rgba(0, 0, 0, 0.05);
- min-height: 100%;
- background-color: #f8fafc;
- position: relative;
- overflow: hidden;
+
+ .bi-diagram-inners {
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
// ----------------------------
// Diagram node general wrapper
@@ -78,15 +78,19 @@
height: 100%;
z-index: 0;
position: absolute;
+ overflow: visible;
top: 0;
right: 0;
bottom: 0;
left: 0;
+ transform-origin: center;
// ----------------------------
// Segment
// ----------------------------
.bi-diagram-segment {
+ transform-origin: center;
+
path {
stroke: #dae1e7;
stroke-width: 0.25rem;
@@ -108,6 +112,7 @@
// ----------------------------
.bi-diagram-link {
pointer-events: stroke;
+ transform-origin: center;
// link path
.bi-link-path {
diff --git a/src/Diagram/index.js b/src/components/Diagram/index.js
similarity index 100%
rename from src/Diagram/index.js
rename to src/components/Diagram/index.js
diff --git a/src/hooks/useCanvasState.js b/src/hooks/useCanvasState.js
new file mode 100644
index 0000000..ba33fc4
--- /dev/null
+++ b/src/hooks/useCanvasState.js
@@ -0,0 +1,18 @@
+import { useState } from 'react';
+
+const defaultInitialState = {
+ pan: { x: 0, y: 0 },
+ zoom: 1,
+};
+
+/**
+ * TODO: document this thing as it was necessary
+ */
+const useCanvasState = (initialState = defaultInitialState) => {
+ const [pan, onPanChange] = useState(initialState.pan);
+ const [zoom, onZoomChange] = useState(initialState.zoom);
+
+ return [{ pan, zoom }, { onPanChange, onZoomChange }];
+};
+
+export default useCanvasState;
diff --git a/src/hooks/useSchema/actionTypes.js b/src/hooks/useSchema/actionTypes.js
index 21acf6a..0a56959 100644
--- a/src/hooks/useSchema/actionTypes.js
+++ b/src/hooks/useSchema/actionTypes.js
@@ -2,3 +2,4 @@ export const ON_CHANGE = 'bi-diagram/useSchema/change';
export const ON_NODE_ADD = 'bi-diagram/useSchema/node/add';
export const ON_NODE_REMOVE = 'bi-diagram/useSchema/node/remove';
export const ON_CONNECT = 'bi-diagram/useSchema/connect';
+export const ON_DISCONNECT = 'bi-diagram/useSchema/disconnect';
diff --git a/src/hooks/useSchema/schemaReducer.js b/src/hooks/useSchema/schemaReducer.js
index ac37e6a..6da06f3 100644
--- a/src/hooks/useSchema/schemaReducer.js
+++ b/src/hooks/useSchema/schemaReducer.js
@@ -1,5 +1,6 @@
import findIndex from 'lodash.findindex';
-import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes';
+import isEqual from 'lodash.isequal';
+import { ON_CHANGE, ON_CONNECT, ON_DISCONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes';
import getNodePortsId from '../../shared/functions/getNodePortsId';
/**
@@ -45,6 +46,11 @@ const schemaReducer = (state, action) => {
nodes: state.nodes || [],
links: state.links || [],
});
+ case ON_DISCONNECT:
+ return ({
+ nodes: state.nodes || [],
+ links: state.links.filter((link) => !isEqual(link, action.payload.link)) || [],
+ });
default:
return state;
}
diff --git a/src/hooks/useSchema/useSchema.js b/src/hooks/useSchema/useSchema.js
index 76c7401..917e6d8 100644
--- a/src/hooks/useSchema/useSchema.js
+++ b/src/hooks/useSchema/useSchema.js
@@ -1,7 +1,7 @@
import { useReducer, useCallback } from 'react';
import ensureNodeId from '../../shared/functions/ensureNodeId';
import schemaReducer from './schemaReducer';
-import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes';
+import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE, ON_DISCONNECT } from './actionTypes';
const initialState = { nodes: [], links: [] };
@@ -17,8 +17,9 @@ const useSchema = (initialSchema = initialState) => {
const addNode = useCallback((node) => dispatch({ type: ON_NODE_ADD, payload: { node: ensureNodeId(node) } }), []);
const removeNode = useCallback((node) => dispatch({ type: ON_NODE_REMOVE, payload: { nodeId: node.id } }), []);
const connect = useCallback((input, output) => dispatch({ type: ON_CONNECT, payload: { link: { input, output } } }), []);
+ const disconnect = useCallback((input, output) => dispatch({ type: ON_DISCONNECT, payload: { link: { input, output } } }), []);
- return [schema, Object.freeze({ onChange, addNode, removeNode, connect })];
+ return [schema, Object.freeze({ onChange, addNode, removeNode, connect, disconnect })];
};
/* eslint-enable max-len */
diff --git a/src/index.js b/src/index.js
index 0bcc28d..3def994 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,10 @@
-import Diagram from './Diagram';
+import Diagram from './components/Diagram';
-export { default as Diagram } from './Diagram';
+export { default as Canvas } from './components/Canvas';
+export { default as Diagram } from './components/Diagram';
+export { default as CanvasControls } from './components/CanvasControls';
export { default as useSchema } from './hooks/useSchema';
+export { default as useCanvasState } from './hooks/useCanvasState';
export { default as createSchema } from './shared/functions/createSchema';
export { validateNode } from './shared/functions/validators';
export { validateNodes } from './shared/functions/validators';
diff --git a/src/shared/Constants.js b/src/shared/Constants.js
new file mode 100644
index 0000000..8af066d
--- /dev/null
+++ b/src/shared/Constants.js
@@ -0,0 +1,16 @@
+export const isTouch = 'ontouchstart' in window;
+
+export const noop = () => undefined;
+
+/**
+ * TODO: explain why on earth you'd do something like this
+ */
+export const Events = Object.freeze({
+ MOUSE_START: isTouch ? 'touchstart' : 'mousedown',
+ MOUSE_MOVE: isTouch ? 'touchmove' : 'mousemove',
+ MOUSE_END: isTouch ? 'touchend' : 'mouseup',
+ DOUBLE_CLICK: 'dblclick',
+ WHEEL: 'wheel',
+});
+
+export const stopPropagation = (e) => e.stopPropagation();
diff --git a/src/shared/functions/pipe.js b/src/shared/functions/pipe.js
new file mode 100644
index 0000000..8209382
--- /dev/null
+++ b/src/shared/functions/pipe.js
@@ -0,0 +1,3 @@
+const pipe = (...fns) => (x) => fns.reduce((y, f) => f(y), x);
+
+export default pipe;
diff --git a/src/shared/internal_hooks/useCanvas.js b/src/shared/internal_hooks/useCanvas.js
index 3e203d1..e64965b 100644
--- a/src/shared/internal_hooks/useCanvas.js
+++ b/src/shared/internal_hooks/useCanvas.js
@@ -5,9 +5,9 @@ import DiagramContext from '../../Context/DiagramContext';
* Returns the canvas bounding box from the DiagramContext
*/
const useCanvas = () => {
- const { canvas } = useContext(DiagramContext);
+ const { canvas, panVal, scaleVal } = useContext(DiagramContext);
- return canvas;
+ return { canvas, panVal, scaleVal };
};
export default useCanvas;
diff --git a/tests/Diagram.spec.js b/tests/Diagram.spec.js
index ea72916..1db8695 100644
--- a/tests/Diagram.spec.js
+++ b/tests/Diagram.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import Diagram from '../dist/Diagram';
+import Diagram from '../dist/components/Diagram';
describe('Diagram component', () => {
afterEach(cleanup);
diff --git a/tests/DiagramCanvas.spec.js b/tests/DiagramCanvas.spec.js
index 38409e2..1fc06f0 100644
--- a/tests/DiagramCanvas.spec.js
+++ b/tests/DiagramCanvas.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import DiagramCanvas from '../dist/Diagram/DiagramCanvas/DiagramCanvas';
+import DiagramCanvas from '../dist/components/Diagram/DiagramCanvas/DiagramCanvas';
describe('DiagramCanvas component', () => {
afterEach(cleanup);
diff --git a/tests/DiagramNode.spec.js b/tests/DiagramNode.spec.js
index 470bf12..6355e71 100644
--- a/tests/DiagramNode.spec.js
+++ b/tests/DiagramNode.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import DiagramContext from '../dist/Context/DiagramContext';
-import DiagramNode from '../dist/Diagram/DiagramNode/DiagramNode';
+import DiagramNode from '../dist/components/Diagram/DiagramNode/DiagramNode';
describe('DiagramNode component', () => {
afterEach(cleanup);
diff --git a/tests/Link.spec.js b/tests/Link.spec.js
index 0ebd19f..269bd0e 100644
--- a/tests/Link.spec.js
+++ b/tests/Link.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import DiagramContext from '../dist/Context/DiagramContext';
-import DiagramLink from '../dist/Diagram/Link/Link';
+import DiagramLink from '../dist/components/Diagram/Link/Link';
describe('Link component', () => {
afterEach(cleanup);
diff --git a/tests/LinkLabel.spec.js b/tests/LinkLabel.spec.js
index 03e656d..5026772 100644
--- a/tests/LinkLabel.spec.js
+++ b/tests/LinkLabel.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import LinkLabel from '../dist/Diagram/Link/LinkLabel';
+import LinkLabel from '../dist/components/Diagram/Link/LinkLabel';
describe('LinkLabel component', () => {
afterEach(cleanup);
diff --git a/tests/NodesCanvas.spec.js b/tests/NodesCanvas.spec.js
index 1c72d00..bb525c5 100644
--- a/tests/NodesCanvas.spec.js
+++ b/tests/NodesCanvas.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import NodesCanvas from '../dist/Diagram/NodesCanvas/NodesCanvas';
+import NodesCanvas from '../dist/components/Diagram/NodesCanvas/NodesCanvas';
describe('NodesCanvas component', () => {
afterEach(cleanup);
diff --git a/tests/Port.spec.js b/tests/Port.spec.js
index 3f7ba67..4321862 100644
--- a/tests/Port.spec.js
+++ b/tests/Port.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import Port from '../dist/Diagram/Port/Port';
+import Port from '../dist/components/Diagram/Port/Port';
describe('Port component', () => {
afterEach(cleanup);
diff --git a/tests/Segment.spec.js b/tests/Segment.spec.js
index bdbbb6b..0a25f40 100644
--- a/tests/Segment.spec.js
+++ b/tests/Segment.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, cleanup } from '@testing-library/react';
-import Segment from '../dist/Diagram/Segment/Segment';
+import Segment from '../dist/components/Diagram/Segment/Segment';
describe('Segment component', () => {
afterEach(cleanup);
diff --git a/tests/findInvolvedEntity.spec.js b/tests/findInvolvedEntity.spec.js
index 6bc3d59..ff8a3c9 100644
--- a/tests/findInvolvedEntity.spec.js
+++ b/tests/findInvolvedEntity.spec.js
@@ -1,4 +1,4 @@
-import findInvolvedEntity from '../dist/Diagram/LinksCanvas/findInvolvedEntity';
+import findInvolvedEntity from '../dist/components/Diagram/LinksCanvas/findInvolvedEntity';
describe('findInvolvedEntity utility function', () => {
it('should be a function', () => {
diff --git a/tests/getEntityCoordinates.spec.js b/tests/getEntityCoordinates.spec.js
index f80900a..0dfe835 100644
--- a/tests/getEntityCoordinates.spec.js
+++ b/tests/getEntityCoordinates.spec.js
@@ -1,4 +1,4 @@
-import getEntityCoordinates from '../dist/Diagram/Link/getEntityCoordinates';
+import getEntityCoordinates from '../dist/components/Diagram/Link/getEntityCoordinates';
describe('getEntityCoordinates function', () => {
const portRefs = { 'port-foo': document.createElement('div') };
diff --git a/tests/removeLinkFromArray.spec.js b/tests/removeLinkFromArray.spec.js
index 050be67..7a3aac8 100644
--- a/tests/removeLinkFromArray.spec.js
+++ b/tests/removeLinkFromArray.spec.js
@@ -1,4 +1,4 @@
-import removeLinkFromArray from '../dist/Diagram/LinksCanvas/removeLinkFromArray';
+import removeLinkFromArray from '../dist/components/Diagram/LinksCanvas/removeLinkFromArray';
describe('removeLinkFromArray utility function', () => {
it('should be a function', () => {
diff --git a/tests/updateNodeCoordinates.spec.js b/tests/updateNodeCoordinates.spec.js
index bd268c5..59bca76 100644
--- a/tests/updateNodeCoordinates.spec.js
+++ b/tests/updateNodeCoordinates.spec.js
@@ -1,4 +1,4 @@
-import updateNodeCoordinates from '../dist/Diagram/NodesCanvas/updateNodeCoordinates';
+import updateNodeCoordinates from '../dist/components/Diagram/NodesCanvas/updateNodeCoordinates';
describe('updateNodeCoordinates utility function', () => {
it('should be a function', () => {
diff --git a/tests/useCanvas.spec.js b/tests/useCanvas.spec.js
index 383a3e3..ba9d832 100644
--- a/tests/useCanvas.spec.js
+++ b/tests/useCanvas.spec.js
@@ -1,7 +1,7 @@
import useCanvas from '../dist/shared/internal_hooks/useCanvas';
// TODO: test this hook
-describe('useCanvas hook', () => {
+describe('useCanvasState hook', () => {
it('should be a function', () => {
expect(useCanvas).to.be.a('function');
});
diff --git a/tests/useSchema.spec.js b/tests/useSchema.spec.js
index 7f44069..6c292d0 100644
--- a/tests/useSchema.spec.js
+++ b/tests/useSchema.spec.js
@@ -1,7 +1,7 @@
import useSchema from '../dist/hooks/useSchema';
import schemaReducer from '../dist/hooks/useSchema/schemaReducer';
import createSchema from '../dist/shared/functions/createSchema';
-import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from '../dist/hooks/useSchema/actionTypes';
+import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE, ON_DISCONNECT } from '../dist/hooks/useSchema/actionTypes';
describe('useSchema reducer', () => {
it('should return the same state if the action is invalid', () => {
@@ -96,6 +96,40 @@ describe('useSchema reducer', () => {
expect(nextSchema.nodes).to.deep.equal([]);
expect(nextSchema.links).to.deep.equal([link]);
});
+
+ it('should remove the link between two nodes ON_DISCONNECT action', () => {
+ const schema = createSchema({
+ nodes: [
+ {
+ id: 'node-1',
+ content: 'Node 1',
+ coordinates: [1, 0],
+ outputs: [{ id: 'port-1' }],
+ },
+ {
+ id: 'node-2',
+ content: 'Node 2',
+ coordinates: [2, 0],
+ inputs: [{ id: 'port-2' }],
+ },
+ {
+ id: 'node-3',
+ content: 'Node 3',
+ coordinates: [3, 0],
+ inputs: [{ id: 'port-3' }],
+ },
+ ],
+ links: [{ input: 'port-2', output: 'port-1' }, { input: 'port-3', output: 'port-1' }],
+ });
+ const link = { input: 'port-3', output: 'port-1' };
+
+ const nextSchema = schemaReducer(schema, { type: ON_DISCONNECT, payload: { link } });
+
+ expect(nextSchema).to.have.property('nodes');
+ expect(nextSchema).to.have.property('links');
+ expect(nextSchema.nodes).to.deep.equal(schema.nodes);
+ expect(nextSchema.links).to.deep.equal([{ input: 'port-2', output: 'port-1' }]);
+ });
});
// TODO: test this hook