diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..4f06b0c
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,6 @@
+{
+ "presets": [
+ "@babel/preset-env",
+ "@babel/preset-react"
+ ]
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fcace55..325c159 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,7 +45,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare distribution
- run: cp -r ./@types ./dist/@types && cp package.json index.d.ts README.md LICENSE CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md ./dist
+ run: cp -r ./@types ./dist/@types && cp package.json index.d.ts CanvasControls.md LICENSE CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md ./dist
- name: Publish
run: |
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 7535f8e..83c3e86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -134,3 +134,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- First implementation of draggable canvas
- First implementation of zoomable canvas
+
+## [0.6.0] - 2020-11-24
+
+### 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/drag&zoom.md b/docs/drag&zoom.md
deleted file mode 100644
index 7c1f8a7..0000000
--- a/docs/drag&zoom.md
+++ /dev/null
@@ -1,73 +0,0 @@
-It is possible to create big diagram and navigate into it using `draggable` prop.
-This prop will let you to move the diagram canvas in every direction till its limits.
-
-``` jsx
-import Diagram, { createSchema, useSchema } from 'beautiful-react-diagrams';
-
-// the diagram model
-const initialSchema = createSchema({
- nodes: [
- { id: 'node-1', content: 'Node 1', coordinates: [2450, 2350], },
- { id: 'node-2', content: 'Node 2', coordinates: [2200, 2500], },
- { id: 'node-3', content: 'Node 3', coordinates:[2450, 2500], },
- { id: 'node-4', content: 'Node 4', coordinates: [2700, 2500], },
- ],
- 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 (
-
-
-
- );
-};
-
-
-```
-
-It is possible to zoom into the diagram using two props:
-- `showZoomButtons`: that will let you zoom into the diagram using buttons.
-- `zoomOnWheel`: that will let you zoom into the diagram using the mouse wheel.
-It is possible to change the zoom button position using `zoomButtonsPosition` that accepts one of the following values:
-`top-left`, `top-right`, `top-center`, `bottom-right`, `bottom-center`, `bottom-left`.
-The default value for `zoomButtonsPosition` is `bottom-right`.
-
-``` jsx
-import Diagram, { createSchema, useSchema } from 'beautiful-react-diagrams';
-
-// the diagram model
-const initialSchema = createSchema({
- nodes: [
- { id: 'node-1', content: 'Node 1', coordinates: [2450, 2350], },
- { id: 'node-2', content: 'Node 2', coordinates: [2200, 2500], },
- { id: 'node-3', content: 'Node 3', coordinates:[2450, 2500], },
- { id: 'node-4', content: 'Node 4', coordinates: [2700, 2500], },
- ],
- 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/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 1cd1724..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,50 +29,48 @@ module.exports = {
sectionDepth: 1,
},
{
- name: 'Canvas',
- content: '../../src/components/Canvas/README.md',
+ name: 'Basic Usage',
+ content: '../Basic-usage.md',
sectionDepth: 1,
},
{
- name: 'Diagram Component',
- content: '../../src/components/Diagram/README.md',
+ name: 'Links and Ports',
+ content: '../Links-Ports.md',
sectionDepth: 1,
},
{
- name: 'Schema',
- content: '../schema.md',
+ name: 'Canvas Controls',
+ content: '../CanvasControls.md',
sectionDepth: 1,
},
{
- name: 'Linking nodes',
- content: '../links.md',
+ name: 'Customisation',
+ content: '../customisation.md',
sectionDepth: 1,
},
{
- name: 'Ports',
- content: '../ports.md',
+ name: 'Dynamic nodes',
+ content: '../dynamic-nodes.md',
sectionDepth: 1,
},
{
- name: 'Customisation',
- content: '../customisation.md',
+ name: 'Schema utilities',
+ content: '../Schema-utils.md',
sectionDepth: 1,
},
-
- { divider: true },
{
- name: 'Dynamic nodes',
- content: '../dynamic-nodes.md',
+ name: 'useSchema',
+ content: '../useSchema.md',
sectionDepth: 1,
},
{
- name: 'Drag & Zoom',
- content: '../drag&zoom.md',
+ name: 'useCanvasState',
+ content: '../useCanvasState.md',
sectionDepth: 1,
},
{
- name: 'Hooks',
- content: '../hooks.md',
+ 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/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 f8b8de9..34fe2d4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "beautiful-react-diagrams",
- "version": "0.5.0",
+ "version": "0.6.0",
"description": "A tiny collection of lightweight React components to build diagrams with ease",
"main": "index.js",
"module": "esm/index.js",
@@ -90,7 +90,6 @@
"beautiful-react-hooks": "^0.31.0",
"classnames": "^2.2.6",
"lodash.findindex": "^4.6.0",
- "lodash.inrange": "3.3.6",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"prop-types": "^15.7.2"
diff --git a/src/components/Canvas/BackgroundGrid.js b/src/components/Canvas/BackgroundGrid.js
index 1b263f5..aaa6ae2 100644
--- a/src/components/Canvas/BackgroundGrid.js
+++ b/src/components/Canvas/BackgroundGrid.js
@@ -6,7 +6,7 @@ const calcCoordinates = (x, y) => ([x * parallaxRatio, y * parallaxRatio]);
const calcTransformation = (x, y, scale) => (`scale(${scale}) translate(${x}, ${y})`);
/**
- * Canvas background
+ * TODO: document me
*/
const BackgroundGrid = ({ translateX, translateY, scale, svgPatternColor, svgPatternOpacity }) => {
const [x, y] = useMemo(() => calcCoordinates(translateX, translateY), [translateX, translateY]);
@@ -23,7 +23,6 @@ const BackgroundGrid = ({ translateX, translateY, scale, svgPatternColor, svgPat
-
diff --git a/src/components/Canvas/Canvas.js b/src/components/Canvas/Canvas.js
index bab84a6..8202878 100644
--- a/src/components/Canvas/Canvas.js
+++ b/src/components/Canvas/Canvas.js
@@ -1,13 +1,15 @@
import React, { useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import useCanvasPan from './useCanvasPan';
-import useCanvasZoom from './useCanvasZoom';
+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 makeStyle = (scale = 1, { x = 0, y = 0 }) => ({
+const calcTransformation = (scale = 1, { x = 0, y = 0 }) => ({
transform: `translate(${x}px, ${y}px) translateZ(0) scale(${scale})`,
});
@@ -16,51 +18,87 @@ const makeStyle = (scale = 1, { x = 0, y = 0 }) => ({
*/
const Canvas = (props) => {
const {
- initialZoom, maxZoom, minZoom, zoomable, pannable, zoomOnWheel, inertia, debug, children, className,
- ElementRenderer, GridRenderer, ...rest
+ pan, onPanChange, zoom, onZoomChange, maxZoom, minZoom, zoomOnWheel, inertia, zoomResetOnDblClick,
+ ElementRenderer, GridRenderer, debug, className, children, ...rest
} = props;
const elRef = useRef();
- const [pan, startPan] = useCanvasPan({ pannable, inertia });
- const [scale] = useCanvasZoom(elRef, { initialZoom, maxZoom, minZoom, zoomable, zoomOnWheel });
const classList = useMemo(() => classNames('bi bi-diagram bi-diagram-canvas', className), [className]);
- const style = useMemo(() => makeStyle(scale, pan), [scale, pan.x, pan.y]);
+ 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 (
-
+
- {children}
+ {filterControlsOut(children)}
{debug && (
-
{JSON.stringify(pan)}
-
{`Scale: ${scale}`}
+
{`Pan: ${pan.x}, ${pan.y}`}
+
{`Scale: ${zoom}`}
)}
+ {enrichControls(children, { onPanChange, onZoomChange, minZoom, maxZoom })}
);
};
Canvas.propTypes = {
- zoomable: PropTypes.bool,
- pannable: PropTypes.bool,
- initialZoom: PropTypes.number,
+ /**
+ * 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 = {
- zoomable: true,
- pannable: true,
- initialZoom: 1,
+ pan: { x: 0, y: 0 },
+ onPanChange: noop,
+ zoom: 1,
+ onZoomChange: noop,
zoomOnWheel: true,
- maxZoom: 5,
- minZoom: 0.4,
+ maxZoom: 2,
+ minZoom: 0.5,
+ zoomResetOnDblClick: true,
inertia: true,
debug: false,
GridRenderer: BackgroundGrid,
diff --git a/src/components/Canvas/README.md b/src/components/Canvas/README.md
deleted file mode 100644
index ce3845a..0000000
--- a/src/components/Canvas/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-```
-import { Canvas } from 'beautiful-react-diagrams';
-
-
-```
diff --git a/src/components/Canvas/childrenUtils.js b/src/components/Canvas/childrenUtils.js
new file mode 100644
index 0000000..02fe4bc
--- /dev/null
+++ b/src/components/Canvas/childrenUtils.js
@@ -0,0 +1,18 @@
+import { Children, cloneElement } from 'react';
+import CanvasControls from '../CanvasControls';
+
+// todo: document this method
+export const filterControlsOut = (children) => Children.map(children, (C) => (C.type !== CanvasControls ? C : 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/useCanvasPan.js b/src/components/Canvas/useCanvasPanHandlers.js
similarity index 69%
rename from src/components/Canvas/useCanvasPan.js
rename to src/components/Canvas/useCanvasPanHandlers.js
index fedb0a9..79e0756 100644
--- a/src/components/Canvas/useCanvasPan.js
+++ b/src/components/Canvas/useCanvasPanHandlers.js
@@ -1,33 +1,30 @@
-import { useState, useCallback, useRef } from 'react';
-import Events from '../../shared/Events';
-import { isTouch } from '../../shared/Constants';
+import { useCallback, useRef } from 'react';
+import { Events, isTouch } from '../../shared/Constants';
-const initialState = { x: 0, y: 0 };
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 getDelta = (point, lastPoint) => ({ x: lastPoint.x - point.x, y: lastPoint.y - point.y });
+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-pannable--zoomable-canvasdi
+ * https://jclem.net/posts/pan-zoom-canvas-react?utm_campaign=building-a-s--zoomable-canvasdi
*/
-const useCanvasPan = ({ pannable, inertia }) => {
- const [pan, setPan] = useState(initialState);
- const lastPointRef = useRef(initialState);
+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 (pannable) {
+ if (onPanChange) {
const lastPoint = { ...lastPointRef.current };
const point = getEventPoint(event);
lastPointRef.current = point;
- setPan(({ x, y }) => {
- const delta = getDelta(lastPoint, point);
+ onPanChange(({ x, y }) => {
+ const delta = calculateDelta(lastPoint, point);
deltaRef.current = { ...delta };
return { x: x + delta.x, y: y + delta.y };
@@ -38,7 +35,7 @@ const useCanvasPan = ({ pannable, inertia }) => {
// TODO: document this callback
const performInertia = useCallback(() => {
if (inertia) {
- setPan(({ x, y }) => ({ x: x + deltaRef.current.x, y: y + deltaRef.current.y }));
+ 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);
@@ -51,7 +48,7 @@ const useCanvasPan = ({ pannable, inertia }) => {
// TODO: document this callback
const endPan = useCallback(() => {
- if (pannable) {
+ if (onPanChange) {
document.removeEventListener(Events.MOUSE_MOVE, performPan);
document.removeEventListener(Events.MOUSE_END, endPan);
@@ -59,18 +56,18 @@ const useCanvasPan = ({ pannable, inertia }) => {
requestAnimationFrame(performInertia);
}
}
- }, [performPan]);
+ }, [performPan, inertia, onPanChange]);
// TODO: document this callback
const onPanStart = useCallback((event) => {
- if (pannable) {
+ if (onPanChange) {
document.addEventListener(Events.MOUSE_MOVE, performPan);
document.addEventListener(Events.MOUSE_END, endPan);
lastPointRef.current = getEventPoint(event);
}
- }, [performPan, endPan]);
+ }, [onPanChange, performPan, endPan]);
- return [pan, onPanStart];
+ return onPanStart;
};
-export default useCanvasPan;
+export default useCanvasPanHandlers;
diff --git a/src/components/Canvas/useCanvasZoom.js b/src/components/Canvas/useCanvasZoomHandlers.js
similarity index 53%
rename from src/components/Canvas/useCanvasZoom.js
rename to src/components/Canvas/useCanvasZoomHandlers.js
index b9360c1..afe8c21 100644
--- a/src/components/Canvas/useCanvasZoom.js
+++ b/src/components/Canvas/useCanvasZoomHandlers.js
@@ -1,5 +1,5 @@
-import { useState, useEffect, useCallback } from 'react';
-import Events from '../../shared/Events';
+import { useEffect, useCallback } from 'react';
+import { Events } from '../../shared/Constants';
// TODO: move to the hooks library
const useEvent = (ref, event, callback, options) => {
@@ -16,23 +16,21 @@ const useEvent = (ref, event, callback, options) => {
}, [ref.current]);
};
-const defaultOptions = { initialZoom: 1, maxZoom: 5, minZoom: 0.4 };
+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 useCanvasZoom = (ref, options = defaultOptions) => {
- const { initialZoom, maxZoom, minZoom, zoomable, zoomOnWheel } = options;
- const [scale, setScale] = useState(initialZoom);
+const useCanvasZoomHandlers = (ref, options = defaultOptions) => {
+ const { onZoomChange, maxZoom, minZoom, zoomOnWheel, zoomResetOnDblClick } = options;
const scaleOnWheel = useCallback((event) => {
- if (zoomable && zoomOnWheel) {
- event.preventDefault(); // FIXME: double check on event bubbling
+ if (onZoomChange && zoomOnWheel) {
+ event.preventDefault(); // FIXME: double check the bubbling of this event you know nothing about
- setScale((currentScale) => {
- // Adjust up to or down to the maximum or minimum scale levels by `interval`.
+ onZoomChange((currentScale) => {
if (event.deltaY > 0) {
return (currentScale + wheelOffset < maxZoom) ? (currentScale + wheelOffset) : maxZoom;
}
@@ -40,11 +38,17 @@ const useCanvasZoom = (ref, options = defaultOptions) => {
return (currentScale - wheelOffset > minZoom) ? (currentScale - wheelOffset) : minZoom;
});
}
- }, [zoomable, setScale, maxZoom, minZoom]);
+ }, [onZoomChange, maxZoom, minZoom]);
- useEvent(ref, Events.WHEEL, scaleOnWheel, { passive: false });
+ const resetZoom = useCallback((event) => {
+ if (onZoomChange && zoomResetOnDblClick) {
+ event.preventDefault();
+ onZoomChange(1);
+ }
+ }, []);
- return [scale, setScale];
+ useEvent(ref, Events.WHEEL, scaleOnWheel, { passive: false });
+ useEvent(ref, Events.DOUBLE_CLICK, resetZoom, { passive: false });
};
-export default useCanvasZoom;
+export default useCanvasZoomHandlers;
diff --git a/src/components/CanvasControls/CanvasControls.js b/src/components/CanvasControls/CanvasControls.js
new file mode 100644
index 0000000..bf8cd8c
--- /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, stopPropagation } 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/components/Diagram/DiagramCanvas/DiagramCanvas.js b/src/components/Diagram/DiagramCanvas/DiagramCanvas.js
index 05f0115..9e0444e 100644
--- a/src/components/Diagram/DiagramCanvas/DiagramCanvas.js
+++ b/src/components/Diagram/DiagramCanvas/DiagramCanvas.js
@@ -1,12 +1,20 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { useWindowScroll, useWindowResize, useMouseEvents } from 'beautiful-react-hooks';
+import React, { useEffect, useRef, useState } from 'react';
+import { useWindowScroll, useWindowResize } from 'beautiful-react-hooks';
import isEqual from 'lodash.isequal';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import DiagramContext from '../../../Context/DiagramContext';
-import getDiagramCanvasCoords from './utils/getDiagramCanvasCoords';
-import getCanvasDragLimits from './utils/getCanvasDragLimits';
-import DiagramZoomButtons from '../DiagramZoomButtons/DiagramZoomButtons';
+
+const extrapolateValues = (bbox) => ({
+ bottom: Math.round(bbox.bottom),
+ height: Math.round(bbox.height),
+ left: Math.round(bbox.left),
+ right: Math.round(bbox.right),
+ top: Math.round(bbox.top),
+ width: Math.round(bbox.width),
+ x: Math.round(bbox.x),
+ y: Math.round(bbox.y),
+});
/**
* The DiagramCanvas component provides a context to the Diagram children.
@@ -14,154 +22,37 @@ import DiagramZoomButtons from '../DiagramZoomButtons/DiagramZoomButtons';
* allow links to easily access to a the ports coordinates
*/
const DiagramCanvas = (props) => {
- const {
- children, portRefs, nodeRefs, draggable, delta, zoomButtonsPosition, showZoomButtons,
- maxZoom, minZoom, zoomOnWheel, className, style, ...rest
- } = props;
- const [bbox, setBoundingBox] = useState(null);
+ const { children, portRefs, nodeRefs, className, ...rest } = props;
+ const [bbox, setBoundingBox] = useState();
const canvasRef = useRef();
- const mouseCoords = useRef();
-
- const [isDragging, setIsDragging] = useState(false);
- const { onMouseDown, onMouseMove, onMouseUp, onMouseLeave } = useMouseEvents(canvasRef);
- const [canvasTranslate, setCanvasTranslate] = useState([0, 0]);
- const [canvasScale, setCanvasScale] = useState(1);
- const classList = classNames('bi bi-diagram-canvas', {
- 'enlarge-diagram-canvas': draggable || showZoomButtons || zoomOnWheel,
- }, className);
-
- const wrapperClassList = classNames('bi bi-diagram', {
- isPanning: isDragging,
- pannable: draggable || showZoomButtons || zoomOnWheel,
- });
+ 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 = extrapolateValues(canvasRef.current.getBoundingClientRect());
if (!isEqual(nextBBox, bbox)) {
setBoundingBox(nextBBox);
}
}
};
- // when the canvas is ready and placed within the DOM, update canvasRef coordinates
- // and save its bounding box to be provided down to children component as a context value for future calculations.
- useEffect(() => {
- calculateBBox(canvasRef.current);
- if (draggable || showZoomButtons || zoomOnWheel) {
- const canvasBBox = canvasRef.current.getBoundingClientRect();
- setCanvasTranslate([-(canvasBBox.width / 2), -(canvasBBox.height / 2)]);
- }
- }, [canvasRef.current]);
+ // 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]);
// same on window scroll and resize
- useWindowScroll(() => calculateBBox(canvasRef.current));
- useWindowResize(() => calculateBBox(canvasRef.current));
-
- // save mouse coordinated if diagram is draggable
- onMouseDown((event) => {
- if (draggable) {
- mouseCoords.current = [event.pageX, event.pageY];
- setIsDragging(true);
- }
- });
+ useWindowScroll(calculateBBox);
+ useWindowResize(calculateBBox);
- /**
- * on mouse move update diagram canvas coordinates and save the latest mouse coordinates
- */
- onMouseMove((event) => {
- if (draggable && isDragging) {
- const currentMouseCoords = [event.pageX, event.pageY];
- const deltaXMouse = currentMouseCoords[0] - mouseCoords.current[0];
- const deltaYMouse = currentMouseCoords[1] - mouseCoords.current[1];
- const canvasParent = canvasRef.current.parentElement;
- const canvasParentDim = [canvasParent.offsetWidth, canvasParent.offsetHeight];
- const canvasDim = [canvasRef.current.offsetWidth, canvasRef.current.offsetHeight];
- // get the canvas drag limit
- const [topLimit, rightLimit, bottomLimit, leftLimit] = getCanvasDragLimits(canvasDim, canvasParentDim);
- // start dragging only if the mouse movement is bigger than the delta prop
- // drag the canvas till its limits
- if (deltaXMouse > delta && canvasTranslate[0] <= leftLimit) {
- setCanvasTranslate([canvasTranslate[0] + deltaXMouse, canvasTranslate[1]]);
- mouseCoords.current = [currentMouseCoords[0], mouseCoords.current[1]];
- }
- if (deltaXMouse < -delta && canvasTranslate[0] >= rightLimit) {
- setCanvasTranslate([canvasTranslate[0] + deltaXMouse, canvasTranslate[1]]);
- mouseCoords.current = [currentMouseCoords[0], mouseCoords.current[1]];
- }
- if (deltaYMouse > delta && canvasTranslate[1] <= topLimit) {
- setCanvasTranslate([canvasTranslate[0], canvasTranslate[1] + deltaYMouse]);
- mouseCoords.current = [mouseCoords.current[0], currentMouseCoords[1]];
- }
- if (deltaYMouse < -delta && canvasTranslate[1] >= bottomLimit) {
- setCanvasTranslate([canvasTranslate[0], canvasTranslate[1] + deltaYMouse]);
- mouseCoords.current = [mouseCoords.current[0], currentMouseCoords[1]];
- }
- }
+ // FIXME: horrible hack
+ useEffect(() => {
+ setTimeout(calculateBBox, 120);
});
- const stopDragging = useCallback(() => {
- if (draggable) {
- setIsDragging(false);
- mouseCoords.current = [];
- }
- }, [draggable, setIsDragging]);
-
- onMouseUp(stopDragging);
- onMouseLeave(stopDragging);
-
- const zoomInHandler = useCallback(() => {
- if (canvasScale <= maxZoom) {
- setCanvasScale(canvasScale + 0.1);
- }
- }, [canvasScale, setCanvasScale, maxZoom]);
-
- const resetZoomHandler = useCallback(() => {
- setCanvasScale(1);
- }, [setCanvasScale]);
-
- const zoomOutHandler = useCallback(() => {
- if (canvasScale > minZoom) {
- setCanvasScale(canvasScale - 0.1);
- }
- }, [canvasScale, setCanvasScale, minZoom]);
-
- const zoomOnWheelHandler = useCallback((event) => {
- event.preventDefault();
- if (event.deltaY > 0) {
- zoomInHandler();
- } else {
- zoomOutHandler();
- }
- }, [zoomOutHandler, zoomInHandler]);
-
- const getDiagramStyle = useCallback(() => {
- if (draggable || showZoomButtons || zoomOnWheel) {
- return { ...style, ...getDiagramCanvasCoords(canvasTranslate[0], canvasTranslate[1], canvasScale) };
- }
- return { ...style };
- }, [draggable, showZoomButtons, zoomOnWheel, canvasTranslate[0], canvasTranslate[1], canvasScale]);
-
return (
-
- {(showZoomButtons) && (
-
= maxZoom}
- buttonsPosition={zoomButtonsPosition}
- />
- )}
-
-
+
+
+
{children}
@@ -172,40 +63,13 @@ const DiagramCanvas = (props) => {
DiagramCanvas.propTypes = {
portRefs: PropTypes.shape({}),
nodeRefs: PropTypes.shape({}),
- /**
- * Defines if the user can move the diagram canvas to reach every node
- */
- draggable: PropTypes.bool,
- /**
- * Defines how many pixels the mouse should drag before starting the canvas panning
- */
- delta: PropTypes.number,
- /**
- * Enable the zoom on diagram canvas and show zoom buttons
- */
- showZoomButtons: PropTypes.bool,
- /**
- * Enable zoom on canvas by mouse wheel
- */
- zoomOnWheel: PropTypes.bool,
- // eslint-disable-next-line max-len
- zoomButtonsPosition: PropTypes.oneOf(['top-left', 'top-right', 'top-center', 'bottom-right', 'bottom-center', 'bottom-left']),
className: PropTypes.string,
- minZoom: PropTypes.number,
- maxZoom: PropTypes.number,
};
DiagramCanvas.defaultProps = {
portRefs: {},
nodeRefs: {},
className: '',
- draggable: false,
- delta: 5,
- showZoomButtons: false,
- zoomOnWheel: false,
- zoomButtonsPosition: 'bottom-right',
- minZoom: 1,
- maxZoom: 100,
};
export default React.memo(DiagramCanvas);
diff --git a/src/components/Diagram/DiagramCanvas/utils/getCanvasDragLimits.js b/src/components/Diagram/DiagramCanvas/utils/getCanvasDragLimits.js
deleted file mode 100644
index 2e23c84..0000000
--- a/src/components/Diagram/DiagramCanvas/utils/getCanvasDragLimits.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Given the canvas and its parent dimensions, returns the canvas drag limits
- * @param canvasDim
- * @param canvasParentDim
- * @returns {[number, number, number, number]}
- */
-const getCanvasDragLimits = (canvasDim, canvasParentDim) => {
- const [canvasWidth, canvasHeight] = Array.isArray(canvasDim) ? canvasDim : [0, 0];
- const [parentWidth, parentHeight] = Array.isArray(canvasParentDim) ? canvasParentDim : [0, 0];
- const topLimit = parentHeight > 0 ? -(parentHeight / 2) : 0;
- const rightLimit = canvasWidth > 0 && parentWidth > 0 ? -canvasWidth + (parentWidth / 2) : 0;
- const bottomLimit = canvasHeight > 0 && parentHeight > 0 ? -canvasHeight + (parentHeight / 2) : 0;
- const leftLimit = parentWidth > 0 ? -(parentWidth / 2) : 0;
-
- return [topLimit, rightLimit, bottomLimit, leftLimit];
-};
-
-export default getCanvasDragLimits;
diff --git a/src/components/Diagram/DiagramCanvas/utils/getDiagramCanvasCoords.js b/src/components/Diagram/DiagramCanvas/utils/getDiagramCanvasCoords.js
deleted file mode 100644
index 648fd71..0000000
--- a/src/components/Diagram/DiagramCanvas/utils/getDiagramCanvasCoords.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// Returns an object with the transform style for the diagram canvas
-
-const getDiagramCanvasCoords = (left, top, scaleValue) => {
- const newLeft = left || 0;
- const newTop = top || 0;
- const newScaleValue = scaleValue || 1;
- return ({
- transform: `translate(${newLeft}px, ${newTop}px) scale(${newScaleValue})`,
- });
-};
-
-export default getDiagramCanvasCoords;
diff --git a/src/components/Diagram/DiagramNode/DiagramNode.js b/src/components/Diagram/DiagramNode/DiagramNode.js
index aa92b1f..c79aba5 100644
--- a/src/components/Diagram/DiagramNode/DiagramNode.js
+++ b/src/components/Diagram/DiagramNode/DiagramNode.js
@@ -1,7 +1,6 @@
import React, { useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import inRange from 'lodash.inrange';
import getDiagramNodeStyle from './getDiagramNodeStyle';
import { usePortRegistration, useNodeRegistration } from '../../../shared/internal_hooks/useContextRegistration';
import { PortType } from '../../../shared/Types';
@@ -17,35 +16,29 @@ import useNodeUnregistration from '../../../shared/internal_hooks/useNodeUnregis
const DiagramNode = (props) => {
const {
id, content, coordinates, type, inputs, outputs, data, onPositionChange, onPortRegister, onNodeRemove,
- onDragNewSegment, onMount, onSegmentFail, onSegmentConnect, render, className, disableDrag,
+ onDragNewSegment, onMount, onSegmentFail, onSegmentConnect, render, className,
} = props;
const registerPort = usePortRegistration(inputs, outputs, onPortRegister); // get the port registration method
const { ref, onDragStart, onDrag } = useDrag({ throttleBy: 14 }); // get the drag n drop methods
const dragStartPoint = useRef(coordinates); // keeps the drag start point in a persistent reference
- if (!disableDrag) {
- // when drag starts, save the starting coordinates into the `dragStartPoint` ref
- onDragStart((event) => {
- dragStartPoint.current = coordinates;
- event.stopPropagation();
- });
+ // when drag starts, save the starting coordinates into the `dragStartPoint` ref
+ onDragStart((event) => {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+ dragStartPoint.current = coordinates;
+ });
- // whilst dragging calculates the next coordinates and perform the `onPositionChange` callback
- onDrag((event, info) => {
- if (onPositionChange) {
- event.stopImmediatePropagation();
- event.stopPropagation();
- const nextWidth = dragStartPoint.current[0] - info.offset[0];
- const nextHeight = dragStartPoint.current[1] - info.offset[1];
- const parentDim = [ref.current.parentElement.offsetWidth, ref.current.parentElement.offsetHeight];
- const refDim = [ref.current.offsetWidth, ref.current.offsetHeight];
- if (inRange(nextWidth, 0, parentDim[0] - refDim[0]) && inRange(nextHeight, 0, parentDim[1] - refDim[1])) {
- const nextCoords = [nextWidth, nextHeight];
- onPositionChange(id, nextCoords);
- }
- }
- });
- }
+ // whilst dragging calculates the next coordinates and perform the `onPositionChange` callback
+ onDrag((event, info) => {
+ if (onPositionChange) {
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+ const nextCoords = [dragStartPoint.current[0] - info.offset[0], dragStartPoint.current[1] - info.offset[1]];
+ onPositionChange(id, nextCoords);
+ }
+ });
// on component unmount, remove its references
useNodeUnregistration(onNodeRemove, inputs, outputs, id);
@@ -64,7 +57,7 @@ const DiagramNode = (props) => {
const customRenderProps = { id, render, content, type, inputs: InputPorts, outputs: OutputPorts, data, className };
return (
-
+
{render && typeof render === 'function' && render(customRenderProps)}
{!render && (
<>
@@ -148,7 +141,6 @@ DiagramNode.propTypes = {
* The possible className
*/
className: PropTypes.string,
- disableDrag: PropTypes.bool,
};
DiagramNode.defaultProps = {
@@ -166,7 +158,6 @@ DiagramNode.defaultProps = {
onSegmentFail: undefined,
onSegmentConnect: undefined,
className: '',
- disableDrag: false,
};
export default React.memo(DiagramNode);
diff --git a/src/components/Diagram/DiagramZoomButtons/DiagramZoomButtons.js b/src/components/Diagram/DiagramZoomButtons/DiagramZoomButtons.js
deleted file mode 100644
index 17d8205..0000000
--- a/src/components/Diagram/DiagramZoomButtons/DiagramZoomButtons.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import React, { useRef, useCallback } from 'react';
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import getDiagramZoomButtonsPosition from './getDiagramZoomButtonsPosition';
-
-// style
-import './diagram-button.scss';
-
-const DiagramZoomButtons = (props) => {
- const {
- onZoomIn, onResetZoom, onZoomOut, disableZoomInBtn, disableZoomOutBtn, buttonsPosition, className, ...rest
- } = props;
- const classList = classNames('diagram-zoom-buttons', {
- 'vertical-orientation': !buttonsPosition.includes('center'),
- }, className);
- const buttonsGroupRef = useRef();
-
- const calculateButtonsPosition = useCallback((position) => {
- const parentElement = buttonsGroupRef.current && buttonsGroupRef.current.parentElement;
- const parentDim = parentElement && [parentElement.offsetWidth, parentElement.offsetHeight];
- // eslint-disable-next-line max-len
- const buttonsDim = buttonsGroupRef.current && [buttonsGroupRef.current.offsetWidth, buttonsGroupRef.current.offsetHeight];
- return getDiagramZoomButtonsPosition(buttonsDim, parentDim, position);
- }, [buttonsGroupRef.current]);
-
- return (
-
-
-
-
-
- );
-};
-
-DiagramZoomButtons.propTypes = {
- /**
- * A function to be perform on zoomIn button click
- */
- onZoomIn: PropTypes.func.isRequired,
- /**
- * A function to be perform on zoom reset button click
- */
- onResetZoom: PropTypes.func.isRequired,
- /**
- * A function to be perform on zoom out button click
- */
- onZoomOut: PropTypes.func.isRequired,
- /**
- * Boolean value used to disabled or not the zoom buttons
- */
- disableZoomOutBtn: PropTypes.bool,
- /**
- * Boolean value used to disabled or not the zoom in button
- */
- disableZoomInBtn: PropTypes.bool,
- // eslint-disable-next-line max-len
- buttonsPosition: PropTypes.oneOf(['top-left', 'top-right', 'top-center', 'bottom-right', 'bottom-center', 'bottom-left']),
-
-};
-
-DiagramZoomButtons.defaultProps = {
- disableZoomOutBtn: false,
- disableZoomInBtn: false,
- buttonsPosition: 'bottom-right',
-};
-
-export default DiagramZoomButtons;
diff --git a/src/components/Diagram/DiagramZoomButtons/diagram-button.scss b/src/components/Diagram/DiagramZoomButtons/diagram-button.scss
deleted file mode 100644
index 450ce14..0000000
--- a/src/components/Diagram/DiagramZoomButtons/diagram-button.scss
+++ /dev/null
@@ -1,67 +0,0 @@
-.diagram-zoom-buttons {
- display: inline-flex;
- position: absolute;
- z-index: 9999;
- flex-direction: row;
- border: 0.05rem solid;
- border-radius: 0.5rem;
-
- > button {
- padding: 1rem;
- border: none;
- background-repeat: no-repeat;
- background-position: center;
- background-size: 1.7rem;
- }
-
- .zoom-in-btn {
- background-image: url('icons/zoomIn.png');
- border-top-left-radius: 0.5rem;
- border-bottom-left-radius: 0.5rem;
- border-top-right-radius: 0;
-
- &.disabled {
- opacity: 0.5;
- }
- }
-
- .zoom-out-btn {
- background-image: url('icons/zoomOut.png');
- border-top-right-radius: 0.5rem;
- border-bottom-right-radius: 0.5rem;
- border-bottom-left-radius: 0;
-
- &.disabled {
- opacity: 0.5;
- }
- }
-
- .zoom-reset-btn {
- background-image: url('icons/zoomReset.png');
- border-left: 0.05rem solid lightgray;
- border-right: 0.05rem solid lightgray;
-
- &.disabled {
- opacity: 0.5;
- }
- }
-
- &.vertical-orientation {
- flex-direction: column;
-
- .zoom-in-btn {
- border-top-right-radius: 0.5rem;
- border-bottom-left-radius: 0;
- }
-
- .zoom-out-btn {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0.5rem;
- }
-
- .zoom-reset-btn {
- border-top: 0.05rem solid lightgray;
- border-bottom: 0.05rem solid lightgray;
- }
- }
-}
diff --git a/src/components/Diagram/DiagramZoomButtons/getDiagramZoomButtonsPosition.js b/src/components/Diagram/DiagramZoomButtons/getDiagramZoomButtonsPosition.js
deleted file mode 100644
index 44c108e..0000000
--- a/src/components/Diagram/DiagramZoomButtons/getDiagramZoomButtonsPosition.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Return a transform css attribute object with translate property calculated
- * based on the passed dimensions and position
- * @param buttonsDim
- * @param parentDim
- * @param position
- * @returns {{transform: string}}
- */
-const getDiagramZoomButtonsPosition = (buttonsDim, parentDim, position) => {
- let [left, top] = [0, 0];
- if (!Array.isArray(buttonsDim) || !Array.isArray(parentDim) || buttonsDim.length !== 2 || parentDim.length !== 2) {
- return { transform: `translate(${left}px,${top}px)` };
- }
-
- const translateValues = {
- 'bottom-right': [parentDim[0] - buttonsDim[0], parentDim[1] - buttonsDim[1]],
- 'bottom-left': [0, parentDim[1] - buttonsDim[1]],
- 'bottom-center': [parentDim[0] / 2 - buttonsDim[0] / 2, parentDim[1] - buttonsDim[1]],
- 'top-right': [parentDim[0] - buttonsDim[0], 0],
- 'top-left': [0, 0],
- 'top-center': [parentDim[0] / 2 - buttonsDim[0] / 2, 0],
- };
- [left, top] = translateValues[position];
-
- return { transform: `translate(${left}px,${top}px)` };
-};
-
-export default getDiagramZoomButtonsPosition;
diff --git a/src/components/Diagram/DiagramZoomButtons/icons/zoomIn.png b/src/components/Diagram/DiagramZoomButtons/icons/zoomIn.png
deleted file mode 100644
index 83ecdda..0000000
Binary files a/src/components/Diagram/DiagramZoomButtons/icons/zoomIn.png and /dev/null differ
diff --git a/src/components/Diagram/DiagramZoomButtons/icons/zoomOut.png b/src/components/Diagram/DiagramZoomButtons/icons/zoomOut.png
deleted file mode 100644
index c760e0f..0000000
Binary files a/src/components/Diagram/DiagramZoomButtons/icons/zoomOut.png and /dev/null differ
diff --git a/src/components/Diagram/DiagramZoomButtons/icons/zoomReset.png b/src/components/Diagram/DiagramZoomButtons/icons/zoomReset.png
deleted file mode 100644
index 869095b..0000000
Binary files a/src/components/Diagram/DiagramZoomButtons/icons/zoomReset.png and /dev/null differ
diff --git a/src/components/Diagram/Link/Link.js b/src/components/Diagram/Link/Link.js
index 7bb1bf4..dbef603 100644
--- a/src/components/Diagram/Link/Link.js
+++ b/src/components/Diagram/Link/Link.js
@@ -27,10 +27,10 @@ const Link = (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]);
/* eslint-disable max-len */
+ const inputPoint = getCoords(input, portRefs, nodeRefs, canvas);
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 = getCoords(output, portRefs, nodeRefs, canvas);
/* eslint-enable max-len */
const pathOptions = {
type: (input.type === 'port' || output.type === 'port') ? 'bezier' : 'curve',
diff --git a/src/components/Diagram/README.md b/src/components/Diagram/README.md
deleted file mode 100644
index 0f0afbb..0000000
--- a/src/components/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/Diagram/diagram.scss b/src/components/Diagram/diagram.scss
index 264f246..d539a0e 100644
--- a/src/components/Diagram/diagram.scss
+++ b/src/components/Diagram/diagram.scss
@@ -1,202 +1,188 @@
.bi.bi-diagram {
- &.pannable {
- overflow: hidden;
- }
- height: 100%;
+ box-sizing: border-box;
width: 100%;
+ height: 100%;
- .bi.bi-diagram-canvas {
+ .bi-diagram-inners {
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;
-
- &.enlarge-diagram-canvas {
- width: 312rem;
- height: 312rem;
- cursor: grab;
- top: 50%;
- left: 50%;
- }
+ }
- // ----------------------------
- // Diagram node general wrapper
- // ----------------------------
- .bi.bi-diagram-node {
- box-sizing: content-box;
- position: absolute;
- z-index: 50;
- user-select: none;
+ // ----------------------------
+ // Diagram node general wrapper
+ // ----------------------------
+ .bi.bi-diagram-node {
+ box-sizing: content-box;
+ position: absolute;
+ z-index: 50;
+ user-select: none;
+
+ .bi-port-wrapper {
+ display: flex;
+
+ .bi-input-ports,
+ .bi-output-ports {
+ flex: 1 1;
+
+ .bi-diagram-port {
+ transition: background-color 0.25s ease-in-out;
+ background-color: rgba(0, 0, 0, 0.08);
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-bottom: 0.25rem;
- .bi-port-wrapper {
- display: flex;
-
- .bi-input-ports,
- .bi-output-ports {
- flex: 1 1;
-
- .bi-diagram-port {
- transition: background-color 0.25s ease-in-out;
- background-color: rgba(0, 0, 0, 0.08);
- width: 1.25rem;
- height: 1.25rem;
- margin-bottom: 0.25rem;
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.1);
- }
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1);
}
}
-
- /* stylelint-disable-next-line no-descending-specificity */
- .bi-output-ports .bi-diagram-port {
- margin-left: auto;
- }
}
+ /* stylelint-disable-next-line no-descending-specificity */
+ .bi-output-ports .bi-diagram-port {
+ margin-left: auto;
+ }
+ }
- // ----------------------------
- // Default node
- // ----------------------------
- &.bi-diagram-node-default {
- transition: box-shadow 0.25s ease-in-out, border 0.3s ease-out;
- border: 0.07rem solid #8795a1;
- background-color: #dae1e7;
- color: #606f7b;
- border-radius: 0.25rem;
- box-shadow: 0 0.07rem 0.2rem 0 rgba(0, 0, 0, 0.1), 0 0.07rem 0.125rem 0 rgba(0, 0, 0, 0.06);
- padding: 0.5rem;
- &:hover {
- box-shadow: 0 0.125rem 1rem -0.2rem rgba(0, 0, 0, 0.1), 0 0.25rem 0.3rem -125rem rgba(0, 0, 0, 0.05);
- }
+ // ----------------------------
+ // Default node
+ // ----------------------------
+ &.bi-diagram-node-default {
+ transition: box-shadow 0.25s ease-in-out, border 0.3s ease-out;
+ border: 0.07rem solid #8795a1;
+ background-color: #dae1e7;
+ color: #606f7b;
+ border-radius: 0.25rem;
+ box-shadow: 0 0.07rem 0.2rem 0 rgba(0, 0, 0, 0.1), 0 0.07rem 0.125rem 0 rgba(0, 0, 0, 0.06);
+ padding: 0.5rem;
+
+ &:hover {
+ box-shadow: 0 0.125rem 1rem -0.2rem rgba(0, 0, 0, 0.1), 0 0.25rem 0.3rem -125rem rgba(0, 0, 0, 0.05);
+ }
- .bi-port-wrapper {
- margin-left: -0.5rem;
- margin-right: -0.5rem;
- }
+ .bi-port-wrapper {
+ margin-left: -0.5rem;
+ margin-right: -0.5rem;
}
}
+ }
+
+ // ----------------------------
+ // Link canvas layer
+ // ----------------------------
+ .bi-link-canvas-layer {
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ position: absolute;
+ overflow: visible;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
// ----------------------------
- // Link canvas layer
+ // Segment
// ----------------------------
- .bi-link-canvas-layer {
- pointer-events: none;
- width: 100%;
- height: 100%;
- z-index: 0;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- // ----------------------------
- // Segment
- // ----------------------------
- .bi-diagram-segment {
- path {
- stroke: #dae1e7;
- stroke-width: 0.25rem;
- stroke-dasharray: 10, 2;
- fill: transparent;
- animation: BiDashSegmentAnimation 1s linear infinite;
- }
+ .bi-diagram-segment {
+ path {
+ stroke: #dae1e7;
+ stroke-width: 0.25rem;
+ stroke-dasharray: 10, 2;
+ fill: transparent;
+ animation: BiDashSegmentAnimation 1s linear infinite;
+ }
- circle {
- stroke: #dae1e7;
- stroke-width: 0.15rem;
- fill: #88cdff;
- }
+ circle {
+ stroke: #dae1e7;
+ stroke-width: 0.15rem;
+ fill: #88cdff;
}
+ }
- // ----------------------------
- // Link item
- // ----------------------------
- .bi-diagram-link {
+ // ----------------------------
+ // Link item
+ // ----------------------------
+ .bi-diagram-link {
+ pointer-events: stroke;
+
+ // link path
+ .bi-link-path {
+ stroke: #dae1e7;
+ stroke-width: 0.25rem;
pointer-events: stroke;
+ fill: transparent;
+ cursor: pointer;
+ user-select: none;
+ }
- // link path
- .bi-link-path {
- stroke: #dae1e7;
- stroke-width: 0.25rem;
- pointer-events: stroke;
- fill: transparent;
- cursor: pointer;
- user-select: none;
- }
+ // link ghost
+ .bi-link-ghost {
+ pointer-events: stroke;
+ stroke: transparent;
+ stroke-width: 1.2rem;
+ fill: transparent;
+ cursor: pointer;
+ }
+
+ // hover
+ &:hover .bi-link-path {
+ stroke: #88cdff;
+ stroke-dasharray: 10, 2;
+ animation: BiDashSegmentAnimation 1s linear infinite;
+ }
- // link ghost
+ // readonly link
+ &.readonly-link {
.bi-link-ghost {
- pointer-events: stroke;
- stroke: transparent;
- stroke-width: 1.2rem;
- fill: transparent;
- cursor: pointer;
+ cursor: not-allowed;
}
- // hover
- &:hover .bi-link-path {
- stroke: #88cdff;
- stroke-dasharray: 10, 2;
- animation: BiDashSegmentAnimation 1s linear infinite;
+ .bi-link-path {
+ cursor: not-allowed;
+ stroke: #b8c2cc;
+ stroke-dasharray: none;
}
- // readonly link
- &.readonly-link {
- .bi-link-ghost {
- cursor: not-allowed;
- }
-
- .bi-link-path {
- cursor: not-allowed;
- stroke: #b8c2cc;
- stroke-dasharray: none;
- }
-
- &:hover {
- stroke: #b8c2cc;
- stroke-dasharray: none;
- animation: none;
- }
+ &:hover {
+ stroke: #b8c2cc;
+ stroke-dasharray: none;
+ animation: none;
}
+ }
- /* stylelint-disable-next-line */
- foreignObject {
- width: 100%;
- height: 100%;
- overflow: visible;
- pointer-events: none;
- }
+ /* stylelint-disable-next-line */
+ foreignObject {
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+ pointer-events: none;
+ }
- // ----------------------------
- // Link label
- // ----------------------------
- .bi-diagram-link-label {
- display: inline-block;
- color: #fff;
- background-color: #3d4852;
- border-radius: 0.25rem;
- padding: 0.25rem;
- text-align: center;
- font-size: 0.875rem;
- user-select: none;
- min-width: 3rem;
- transform: translate(-50%, -50%);
- }
+ // ----------------------------
+ // Link label
+ // ----------------------------
+ .bi-diagram-link-label {
+ display: inline-block;
+ color: #fff;
+ background-color: #3d4852;
+ border-radius: 0.25rem;
+ padding: 0.25rem;
+ text-align: center;
+ font-size: 0.875rem;
+ user-select: none;
+ min-width: 3rem;
+ transform: translate(-50%, -50%);
}
}
}
}
+
// ----------------------------
// Involved animations
// ----------------------------
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/index.js b/src/index.js
index 4eef99b..3def994 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,9 @@ import Diagram from './components/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
index 192f498..8af066d 100644
--- a/src/shared/Constants.js
+++ b/src/shared/Constants.js
@@ -1,3 +1,16 @@
export const isTouch = 'ontouchstart' in window;
-export default Object.freeze({ isTouch });
+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/Events.js b/src/shared/Events.js
deleted file mode 100644
index d6d2d71..0000000
--- a/src/shared/Events.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { isTouch } from './Constants';
-
-export default Object.freeze({
- MOUSE_START: isTouch ? 'touchstart' : 'mousedown',
- MOUSE_MOVE: isTouch ? 'touchmove' : 'mousemove',
- MOUSE_END: isTouch ? 'touchend' : 'mouseup',
- WHEEL: 'wheel',
-});
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/tests/Diagram.spec.js b/tests/Diagram.spec.js
index bc6c518..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);
@@ -26,21 +26,21 @@ describe('Diagram component', () => {
it('should accept an "id" prop', () => {
const { container } = render(
);
- const wrapper = container.querySelector('.bi.bi-diagram-canvas');
+ const wrapper = container.querySelector('.bi.bi-diagram');
expect(wrapper.id).to.equal('foo');
});
it('should allow adding custom classes', () => {
const { container } = render(
);
- const wrapper = container.querySelector('.bi.bi-diagram-canvas');
+ const wrapper = container.querySelector('.bi.bi-diagram');
expect(wrapper.getAttribute('class').split(' ')).to.include.members(['foo']);
});
it('should allow to define custom style', () => {
const { container } = render(
);
- const wrapper = container.querySelector('.bi.bi-diagram-canvas');
+ const wrapper = container.querySelector('.bi.bi-diagram');
expect(wrapper.getAttribute('style')).to.equal('margin: 10px;');
});
diff --git a/tests/DiagramCanvas.spec.js b/tests/DiagramCanvas.spec.js
index dcedce2..1fc06f0 100644
--- a/tests/DiagramCanvas.spec.js
+++ b/tests/DiagramCanvas.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { render, cleanup, fireEvent } from '@testing-library/react';
-import DiagramCanvas from '../dist/Diagram/DiagramCanvas/DiagramCanvas';
+import { render, cleanup } from '@testing-library/react';
+import DiagramCanvas from '../dist/components/Diagram/DiagramCanvas/DiagramCanvas';
describe('DiagramCanvas component', () => {
afterEach(cleanup);
@@ -18,77 +18,4 @@ describe('DiagramCanvas component', () => {
expect(wrapper.getAttribute('class').split(' ')).to.include.members(['bi', 'bi-diagram']);
});
-
- it('should enlarge the diagram canvas when it is draggable', () => {
- const { container } = render(
);
- const wrapper = container.querySelector('.bi.bi-diagram .bi-diagram-canvas');
-
- expect(wrapper.getAttribute('class').split(' '))
- .to.include.members(['bi', 'bi-diagram-canvas', 'enlarge-diagram-canvas']);
- });
-
- it('should be zoomable', () => {
- const { container } = render(
);
- const zoomButtons = container.querySelector('.diagram-zoom-buttons');
- const zoomInBtn = zoomButtons.querySelector('.zoom-in-btn');
- const zoomResetBtn = zoomButtons.querySelector('.zoom-reset-btn');
- const zoomOutBtn = zoomButtons.querySelector('.zoom-out-btn');
-
- expect(zoomButtons).to.exist;
- expect(zoomInBtn).to.exist;
- expect(zoomOutBtn).to.exist;
- expect(zoomResetBtn).to.exist;
- });
-
- it('should change the css scale value when clicking on zoom buttons', () => {
- const { container } = render(
);
- const zoomButtons = container.querySelector('.diagram-zoom-buttons');
- const wrapper = container.querySelector('.bi.bi-diagram-canvas');
- const zoomInBtn = zoomButtons.querySelector('.zoom-in-btn');
- const zoomResetBtn = zoomButtons.querySelector('.zoom-reset-btn');
- const zoomOutBtn = zoomButtons.querySelector('.zoom-out-btn');
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1)');
-
- fireEvent.click(zoomInBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1.1)');
- fireEvent.click(zoomInBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1.2000000000000002)');
-
- fireEvent.click(zoomOutBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1.1)');
-
- fireEvent.click(zoomResetBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1)');
- });
-
- it('can\'t go over the defined maxZoom or minZoom provided props', () => {
- const { container } = render(
);
- const zoomButtons = container.querySelector('.diagram-zoom-buttons');
- const wrapper = container.querySelector('.bi.bi-diagram-canvas');
- const zoomInBtn = zoomButtons.querySelector('.zoom-in-btn');
- const zoomOutBtn = zoomButtons.querySelector('.zoom-out-btn');
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1)');
-
- fireEvent.click(zoomInBtn);
- fireEvent.click(zoomInBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(1.2000000000000002)');
- expect(zoomInBtn.getAttribute('class').split(' ')).to.include.members(['zoom-in-btn', 'disabled']);
- fireEvent.click(zoomInBtn);
- expect(wrapper.style.transform).to.not.equal('translate(0px, 0px) scale(1.3000000000000003)');
-
- fireEvent.click(zoomOutBtn);
- fireEvent.click(zoomOutBtn);
- fireEvent.click(zoomOutBtn);
- fireEvent.click(zoomOutBtn);
-
- expect(wrapper.style.transform).to.be.equal('translate(0px, 0px) scale(0.9)');
- expect(zoomOutBtn.getAttribute('class').split(' ')).to.include.members(['zoom-out-btn', 'disabled']);
- });
});
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/DiagramZoomButtons.spec.js b/tests/DiagramZoomButtons.spec.js
deleted file mode 100644
index 9a889d1..0000000
--- a/tests/DiagramZoomButtons.spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
-import DiagramZoomButtons from '../dist/Diagram/DiagramZoomButtons/DiagramZoomButtons';
-import noop from './utils/noop';
-
-describe('DiagramZoomButtons component', () => {
- it('should render without explode', () => {
- const { container } = render(
-
,
- );
-
- should.exist(container);
- expect(container.querySelector('div')).to.exist;
- });
-
- it('should render zoom buttons', () => {
- const { container } = render(
-
,
- );
- const zoomInBtn = container.querySelector('.zoom-in-btn');
- const zoomResetBtn = container.querySelector('.zoom-reset-btn');
- const zoomOutBtn = container.querySelector('.zoom-out-btn');
-
- expect(zoomInBtn).to.exist;
- expect(zoomOutBtn).to.exist;
- expect(zoomResetBtn).to.exist;
- });
-
- it('should perform provided zoom functions on buttons click', () => {
- const onZoomIn = sinon.spy();
- const onZoomOut = sinon.spy();
- const onZoomReset = sinon.spy();
- const { container } = render(
-
,
- );
- const zoomInBtn = container.querySelector('.zoom-in-btn');
- const zoomResetBtn = container.querySelector('.zoom-reset-btn');
- const zoomOutBtn = container.querySelector('.zoom-out-btn');
-
- fireEvent.click(zoomInBtn);
-
- expect(onZoomIn.calledOnce).to.be.true;
- fireEvent.click(zoomOutBtn);
-
- expect(onZoomOut.calledOnce).to.be.true;
-
- fireEvent.click(zoomResetBtn);
-
- expect(onZoomReset.calledOnce).to.be.true;
- });
-});
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/getCanvasDragLimits.spec.js b/tests/getCanvasDragLimits.spec.js
index cb9da8e..23d86a1 100644
--- a/tests/getCanvasDragLimits.spec.js
+++ b/tests/getCanvasDragLimits.spec.js
@@ -1,4 +1,4 @@
-import getCanvasDragLimits from '../dist/Diagram/DiagramCanvas/utils/getCanvasDragLimits';
+import getCanvasDragLimits from '../dist/components/Diagram/DiagramCanvas/utils/getCanvasDragLimits';
describe('getCanvasDragLimits function', () => {
it('should be a function', () => {
diff --git a/tests/getDiagramCanvasCoords.spec.js b/tests/getDiagramCanvasCoords.spec.js
index 598d189..7abc6fc 100644
--- a/tests/getDiagramCanvasCoords.spec.js
+++ b/tests/getDiagramCanvasCoords.spec.js
@@ -1,4 +1,4 @@
-import getDiagramCanvasCoords from '../dist/Diagram/DiagramCanvas/utils/getDiagramCanvasCoords';
+import getDiagramCanvasCoords from '../dist/components/Diagram/DiagramCanvas/utils/getDiagramCanvasCoords';
describe('getDiagramCanvasCoords', () => {
it('should be a function', () => {
diff --git a/tests/getDiagramZoomButtonsPosition.spec.js b/tests/getDiagramZoomButtonsPosition.spec.js
deleted file mode 100644
index 995d84a..0000000
--- a/tests/getDiagramZoomButtonsPosition.spec.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import getDiagramZoomButtonsPosition from '../dist/Diagram/DiagramZoomButtons/getDiagramZoomButtonsPosition';
-
-describe('getDiagramZoomButtonsPosition function', () => {
- it('should be a function', () => {
- expect(getDiagramZoomButtonsPosition).to.be.a('function');
- });
-
- it('should returns a transform object', () => {
- const transformObj = getDiagramZoomButtonsPosition([36, 108], [640, 320], 'top-right');
-
- expect(transformObj).to.be.an('object');
- expect(transformObj.transform).to.be.equal('translate(604px,0px)');
- });
-});
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');
});