From c2f73a6fbf9ad8c72ffe8e1f38fae93ddcb3201b Mon Sep 17 00:00:00 2001 From: ginoemiliozzi Date: Wed, 25 Nov 2020 11:48:07 +0000 Subject: [PATCH 1/3] Add disconnect function to useSchema - #31 --- CHANGELOG.md | 6 +++++ package.json | 2 +- src/hooks/useSchema/actionTypes.js | 1 + src/hooks/useSchema/schemaReducer.js | 8 ++++++- src/hooks/useSchema/useSchema.js | 5 ++-- tests/useSchema.spec.js | 36 +++++++++++++++++++++++++++- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7535f8e..3b0e254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,3 +134,9 @@ 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-25 + +### Added + +- Added `disconnect` function exported in `useSchema` hook diff --git a/package.json b/package.json index ab184e2..3a508cb 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", 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/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 From c7e844a957b3886c449ff2ec45b589d87fbe9810 Mon Sep 17 00:00:00 2001 From: Joan Cejudo Date: Fri, 27 Nov 2020 14:04:05 -0500 Subject: [PATCH 2/3] Refactor how event coordinates and targets are obtained to support single touch events --- .../Diagram/Link/getEntityCoordinates.js | 6 +++-- src/components/Diagram/Port/Port.js | 11 ++++---- src/shared/Constants.js | 11 ++++++++ src/shared/functions/getRelativePoint.js | 3 ++- src/shared/internal_hooks/useDrag.js | 27 ++++++++----------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/components/Diagram/Link/getEntityCoordinates.js b/src/components/Diagram/Link/getEntityCoordinates.js index 63d999a..7fe664a 100644 --- a/src/components/Diagram/Link/getEntityCoordinates.js +++ b/src/components/Diagram/Link/getEntityCoordinates.js @@ -13,8 +13,10 @@ const getEntityCoordinates = (entity, portRefs, nodeRefs, canvas) => { if (portRefs && portRefs[entity.entity.id]) { 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( + { x: bbox.x + (bbox.width / 2), y: bbox.y + (bbox.height / 2) }, + { x: canvas.x, y: canvas.y }, + ); } return undefined; diff --git a/src/components/Diagram/Port/Port.js b/src/components/Diagram/Port/Port.js index 5a8fcbb..bea8aa6 100644 --- a/src/components/Diagram/Port/Port.js +++ b/src/components/Diagram/Port/Port.js @@ -3,7 +3,7 @@ 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 { getEventPoint, getEventTarget } from '../../../shared/Constants'; /** * Port * @param props @@ -19,16 +19,15 @@ const Port = (props) => { 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, { x: canvas.x, y: canvas.y }); + const to = getRelativePoint(getEventPoint(event), { x: canvas.x, y: canvas.y }); onDragNewSegment(id, from, to, alignment); } }); onDragEnd((event) => { - const targetPort = event.target.getAttribute('data-port-id'); - if (targetPort && event.target !== ref.current && canLink(id, targetPort, type) && onSegmentConnect) { + const targetPort = getEventTarget(event).getAttribute('data-port-id'); + if (targetPort && getEventTarget(event) !== ref.current && canLink(id, targetPort, type) && onSegmentConnect) { const args = type === 'input' ? [id, targetPort, type] : [targetPort, id, type]; onSegmentConnect(...args); diff --git a/src/shared/Constants.js b/src/shared/Constants.js index 8af066d..5fbc5a6 100644 --- a/src/shared/Constants.js +++ b/src/shared/Constants.js @@ -2,6 +2,17 @@ export const isTouch = 'ontouchstart' in window; export const noop = () => undefined; +const getMouseEventPoint = (e) => ({ x: e.pageX, y: e.pageY }); +const getTouchEventPoint = (e) => getMouseEventPoint(e.changedTouches[0]); +export const getEventPoint = isTouch ? getTouchEventPoint : getMouseEventPoint; + +const getMouseEventTarget = (e) => e.target; +const getTouchEventTarget = (e) => document.elementFromPoint( + e.changedTouches[0].clientX, + e.changedTouches[0].clientY, +); +export const getEventTarget = isTouch ? getTouchEventTarget : getMouseEventTarget; + /** * TODO: explain why on earth you'd do something like this */ diff --git a/src/shared/functions/getRelativePoint.js b/src/shared/functions/getRelativePoint.js index 0f9b38c..19719cb 100644 --- a/src/shared/functions/getRelativePoint.js +++ b/src/shared/functions/getRelativePoint.js @@ -1,3 +1,4 @@ -const getRelativePoint = (point, relative) => [point[0] - relative[0], point[1] - relative[1]]; +// useDrag now transforms the passed mouse/touch event and converts it to an Object => {x: , y: } +const getRelativePoint = (point, relative) => [point.x - relative.x, point.y - relative.y]; export default getRelativePoint; diff --git a/src/shared/internal_hooks/useDrag.js b/src/shared/internal_hooks/useDrag.js index 884ad85..33cd43d 100644 --- a/src/shared/internal_hooks/useDrag.js +++ b/src/shared/internal_hooks/useDrag.js @@ -1,5 +1,6 @@ import throttle from 'lodash.throttle'; import { useRef, useCallback, useEffect } from 'react'; +import { Events, getEventPoint } from '../Constants'; const defaultOptions = { /** @@ -14,13 +15,6 @@ const defaultOptions = { throttleBy: 0, }; -/** - * Returns the click coordinates of a MouseEvent - * @param event - * @returns {*[]} - */ -const getEventCoordinates = (event) => [event.clientX, event.clientY]; - /** * Create a persistent callback reference that will live trough a component lifecycle * @param ref @@ -94,7 +88,7 @@ const useDrag = (options = defaultOptions) => { info.isDragging = true; info.end = null; info.offset = null; - info.start = getEventCoordinates(event); + info.start = getEventPoint(event); if (dragStartHandlerRef.current) { dragStartHandlerRef.current(event, { ...info }); @@ -106,8 +100,9 @@ const useDrag = (options = defaultOptions) => { * Whilst dragging the element, updates the state then perform the user's onDrag handler if exists */ const onDrag = useCallback(throttle((event) => { + const eventPoints = getEventPoint(event); if (info.isDragging) { - info.offset = [info.start[0] - event.clientX, info.start[1] - event.clientY]; + info.offset = [info.start.x - eventPoints.x, info.start.y - eventPoints.y]; if (dragHandlerRef.current) { dragHandlerRef.current(event, { ...info }); @@ -121,7 +116,7 @@ const useDrag = (options = defaultOptions) => { const onDragEnd = useCallback((event) => { if (info.isDragging) { info.isDragging = false; - info.end = getEventCoordinates(event); + info.end = getEventPoint(event); if (dragEndHandlerRef.current) { dragEndHandlerRef.current(event, { ...info }); @@ -140,16 +135,16 @@ const useDrag = (options = defaultOptions) => { /* eslint-enable no-underscore-dangle */ if (targetRef.current) { - targetRef.current.addEventListener('mousedown', _onDragStart); - document.addEventListener('mousemove', _onDrag); - document.addEventListener('mouseup', _onDragEnd); + targetRef.current.addEventListener(Events.MOUSE_START, _onDragStart); + document.addEventListener(Events.MOUSE_MOVE, _onDrag); + document.addEventListener(Events.MOUSE_END, _onDragEnd); } return () => { if (targetRef.current) { - targetRef.current.removeEventListener('mousedown', _onDragStart); - document.removeEventListener('mousemove', _onDrag); - document.removeEventListener('mouseup', _onDragEnd); + targetRef.current.removeEventListener(Events.MOUSE_START, _onDragStart); + document.removeEventListener(Events.MOUSE_MOVE, _onDrag); + document.removeEventListener(Events.MOUSE_END, _onDragEnd); } }; }, [targetRef.current]); From cd69f30a9760024d7a3265b462ee605d963c7fa3 Mon Sep 17 00:00:00 2001 From: Joan Cejudo Date: Fri, 27 Nov 2020 18:23:45 -0500 Subject: [PATCH 3/3] Update unit test for relative point --- tests/getRelativePoint.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/getRelativePoint.spec.js b/tests/getRelativePoint.spec.js index 836c093..ad972a9 100644 --- a/tests/getRelativePoint.spec.js +++ b/tests/getRelativePoint.spec.js @@ -6,6 +6,6 @@ describe('getRelativePoint utility function', () => { }); it('should calculate a point coordinates relatively to another', () => { - expect(getRelativePoint([10, 10], [20, 20])).to.deep.equal([-10, -10]); + expect(getRelativePoint({ x: 10, y: 10 }, { x: 20, y: 20 })).to.deep.equal([-10, -10]); }); });