diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c3e86..5a91103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,10 +135,12 @@ 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 +## [0.6.0] - 2020-11-25 ### Added - Canvas Component for panning and zooming - useCanvas hook - CanvasControl component +- Added `disconnect` function exported in `useSchema` hook + 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/hooks/useSchema/actionTypes.js b/src/hooks/useSchema/actionTypes.js index 21acf6a..0a56959 100644 --- a/src/hooks/useSchema/actionTypes.js +++ b/src/hooks/useSchema/actionTypes.js @@ -2,3 +2,4 @@ export const ON_CHANGE = 'bi-diagram/useSchema/change'; export const ON_NODE_ADD = 'bi-diagram/useSchema/node/add'; export const ON_NODE_REMOVE = 'bi-diagram/useSchema/node/remove'; export const ON_CONNECT = 'bi-diagram/useSchema/connect'; +export const ON_DISCONNECT = 'bi-diagram/useSchema/disconnect'; diff --git a/src/hooks/useSchema/schemaReducer.js b/src/hooks/useSchema/schemaReducer.js index ac37e6a..6da06f3 100644 --- a/src/hooks/useSchema/schemaReducer.js +++ b/src/hooks/useSchema/schemaReducer.js @@ -1,5 +1,6 @@ import findIndex from 'lodash.findindex'; -import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes'; +import isEqual from 'lodash.isequal'; +import { ON_CHANGE, ON_CONNECT, ON_DISCONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes'; import getNodePortsId from '../../shared/functions/getNodePortsId'; /** @@ -45,6 +46,11 @@ const schemaReducer = (state, action) => { nodes: state.nodes || [], links: state.links || [], }); + case ON_DISCONNECT: + return ({ + nodes: state.nodes || [], + links: state.links.filter((link) => !isEqual(link, action.payload.link)) || [], + }); default: return state; } diff --git a/src/hooks/useSchema/useSchema.js b/src/hooks/useSchema/useSchema.js index 76c7401..917e6d8 100644 --- a/src/hooks/useSchema/useSchema.js +++ b/src/hooks/useSchema/useSchema.js @@ -1,7 +1,7 @@ import { useReducer, useCallback } from 'react'; import ensureNodeId from '../../shared/functions/ensureNodeId'; import schemaReducer from './schemaReducer'; -import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE } from './actionTypes'; +import { ON_CHANGE, ON_CONNECT, ON_NODE_ADD, ON_NODE_REMOVE, ON_DISCONNECT } from './actionTypes'; const initialState = { nodes: [], links: [] }; @@ -17,8 +17,9 @@ const useSchema = (initialSchema = initialState) => { const addNode = useCallback((node) => dispatch({ type: ON_NODE_ADD, payload: { node: ensureNodeId(node) } }), []); const removeNode = useCallback((node) => dispatch({ type: ON_NODE_REMOVE, payload: { nodeId: node.id } }), []); const connect = useCallback((input, output) => dispatch({ type: ON_CONNECT, payload: { link: { input, output } } }), []); + const disconnect = useCallback((input, output) => dispatch({ type: ON_DISCONNECT, payload: { link: { input, output } } }), []); - return [schema, Object.freeze({ onChange, addNode, removeNode, connect })]; + return [schema, Object.freeze({ onChange, addNode, removeNode, connect, disconnect })]; }; /* eslint-enable max-len */ diff --git a/src/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]); 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]); }); }); 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