diff --git a/examples/Basic/Basic.vue b/examples/Basic/Basic.vue new file mode 100644 index 00000000..32498af0 --- /dev/null +++ b/examples/Basic/Basic.vue @@ -0,0 +1,102 @@ + + + + + + + reset transform + change pos + toggle classnames + toObject + + + + diff --git a/examples/Basic/index.tsx b/examples/Basic/index.tsx deleted file mode 100644 index ecf6fc77..00000000 --- a/examples/Basic/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import RevueFlow, { - MiniMap, - Controls, - Background, - Connection, - Edge, - Elements, - FlowElement, - Node, - OnLoadParams, - addEdge, - isNode, - removeElements -} from '../../src'; -import { defineComponent, ref } from 'vue'; - -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' } -]; - -const BasicFlow = defineComponent({ - components: { RevueFlow, MiniMap, Controls, Background }, - setup() { - const elements = ref(initialElements); - const rfInstance = ref(null); - const onElementsRemove = (elementsToRemove: Elements) => (elements.value = removeElements(elementsToRemove, elements.value)); - const onConnect = (params: Edge | Connection) => (elements.value = addEdge(params, elements.value)); - const onLoad = (revueFlowInstance: OnLoadParams) => (rfInstance.value = revueFlowInstance); - - const updatePos = () => { - elements.value = elements.value.map((el) => { - if (isNode(el)) { - el.position = { - x: Math.random() * 400, - y: Math.random() * 400 - }; - } - - return el; - }); - }; - - const logToObject = () => console.log(rfInstance.value?.toObject()); - const resetTransform = () => rfInstance.value?.setTransform({ x: 0, y: 0, zoom: 1 }); - - const toggleClassnames = () => { - elements.value = elements.value.map((el) => { - if (isNode(el)) { - el.className = el.className === 'light' ? 'dark' : 'light'; - } - - return el; - }); - }; - - return () => ( - - - - - - - reset transform - - - change pos - - - toggle classnames - - toObject - - - ); - } -}); - -export default BasicFlow; diff --git a/examples/CustomConnectionLine/ConnectionLine.tsx b/examples/CustomConnectionLine/ConnectionLine.vue similarity index 59% rename from examples/CustomConnectionLine/ConnectionLine.tsx rename to examples/CustomConnectionLine/ConnectionLine.vue index 6570adbf..afee9f5e 100644 --- a/examples/CustomConnectionLine/ConnectionLine.tsx +++ b/examples/CustomConnectionLine/ConnectionLine.vue @@ -1,3 +1,16 @@ + + + + + + + diff --git a/examples/CustomConnectionLine/CustomConnectionLine.vue b/examples/CustomConnectionLine/CustomConnectionLine.vue new file mode 100644 index 00000000..0f848b3c --- /dev/null +++ b/examples/CustomConnectionLine/CustomConnectionLine.vue @@ -0,0 +1,36 @@ + + + + + + diff --git a/examples/CustomConnectionLine/index.tsx b/examples/CustomConnectionLine/index.tsx deleted file mode 100644 index 62e400f7..00000000 --- a/examples/CustomConnectionLine/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import RevueFlow, { removeElements, addEdge, Background, BackgroundVariant, Elements, Connection, Edge } from '../../src'; - -import ConnectionLine from './ConnectionLine'; -import { defineComponent, ref } from 'vue'; - -const initialElements: Elements = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }]; - -const ConnectionLineFlow = defineComponent({ - components: { Background }, - setup() { - const elements = ref(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => (elements.value = removeElements(elementsToRemove, elements.value)); - const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value)); - - return () => ( - - - - ); - } -}); - -export default ConnectionLineFlow; diff --git a/examples/CustomNode/ColorSelectorNode.tsx b/examples/CustomNode/ColorSelectorNode.tsx deleted file mode 100644 index 3058fe7e..00000000 --- a/examples/CustomNode/ColorSelectorNode.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { CSSProperties, defineComponent } from 'vue'; -import { Handle, Position, Connection, Edge } from '../../src'; - -const targetHandleStyle: CSSProperties = { background: '#555' }; -const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; -const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' }; - -const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); - -const ColorSelectorNode = defineComponent({ - props: { - data: { - type: Object, - required: false, - default: () => ({}) - } - }, - setup(props) { - return () => ( - <> - - - Custom Color Picker Node: {props.data.color} - - - - - > - ); - } -}); - -export default ColorSelectorNode; diff --git a/examples/CustomNode/ColorSelectorNode.vue b/examples/CustomNode/ColorSelectorNode.vue new file mode 100644 index 00000000..2b88dcc3 --- /dev/null +++ b/examples/CustomNode/ColorSelectorNode.vue @@ -0,0 +1,44 @@ + + + + + Custom Color Picker Node: {{ data.color }} + + + + + + + diff --git a/examples/CustomNode/CustomNode.vue b/examples/CustomNode/CustomNode.vue new file mode 100644 index 00000000..a7a7b08b --- /dev/null +++ b/examples/CustomNode/CustomNode.vue @@ -0,0 +1,152 @@ + + + + + + + diff --git a/examples/CustomNode/index.tsx b/examples/CustomNode/index.tsx deleted file mode 100644 index 5ce2553a..00000000 --- a/examples/CustomNode/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { defineComponent, onMounted, ref } from 'vue'; -import RevueFlow, { - isEdge, - removeElements, - addEdge, - MiniMap, - Controls, - Node, - FlowElement, - OnLoadParams, - Elements, - Position, - SnapGrid, - Connection, - Edge -} from '../../src'; -import ColorSelectorNode from './ColorSelectorNode'; - -const onLoad = (revueFlowInstance: OnLoadParams) => console.log('flow loaded:', revueFlowInstance); -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); - -const initBgColor = '#1A192B'; - -const connectionLineStyle = { stroke: '#fff' }; -const snapGrid: SnapGrid = [16, 16]; -const nodeTypes = { - selectorNode: ColorSelectorNode -}; - -const CustomNodeFlow = defineComponent({ - setup() { - const elements = ref([]); - const bgColor = ref(initBgColor); - - onMounted(() => { - const onChange = (event: Event) => { - elements.value = elements.value.map((e) => { - if (isEdge(e) || e.id !== '2') { - return e; - } - - const color = (event.target as HTMLInputElement).value; - - bgColor.value = color; - - return { - ...e, - data: { - ...e.data, - color - } - }; - }); - }; - - elements.value = [ - { - id: '1', - type: 'input', - data: { label: 'An input node' }, - position: { x: 0, y: 50 }, - sourcePosition: Position.Right - }, - { - id: '2', - type: 'selectorNode', - data: { onChange: onChange, color: initBgColor }, - style: { border: '1px solid #777', padding: 10 }, - position: { x: 250, y: 50 } - }, - { - id: '3', - type: 'output', - data: { label: 'Output A' }, - position: { x: 550, y: 25 }, - targetPosition: Position.Left - }, - { - id: '4', - type: 'output', - data: { label: 'Output B' }, - position: { x: 550, y: 100 }, - targetPosition: Position.Left - }, - - { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, - { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } }, - { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } } - ]; - }); - - const onElementsRemove = (elementsToRemove: Elements) => (elements.value = removeElements(elementsToRemove, elements.value)); - const onConnect = (params: Connection | Edge) => - (elements.value = addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, elements.value)); - - return () => ( - - { - if (n.type === 'input') return '#0041d0'; - if (n.type === 'selectorNode') return bgColor.value; - if (n.type === 'output') return '#ff0072'; - - return '#eee'; - }} - nodeColor={(n: Node): string => { - if (n.type === 'selectorNode') return bgColor.value; - - return '#fff'; - }} - /> - - - ); - } -}); - -export default CustomNodeFlow; diff --git a/examples/DragNDrop/DnD.vue b/examples/DragNDrop/DnD.vue new file mode 100644 index 00000000..b477ad13 --- /dev/null +++ b/examples/DragNDrop/DnD.vue @@ -0,0 +1,93 @@ + + + + + + + + + + + diff --git a/examples/DragNDrop/index.tsx b/examples/DragNDrop/index.tsx deleted file mode 100644 index fe4eebaf..00000000 --- a/examples/DragNDrop/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import RevueFlow, { - addEdge, - removeElements, - Controls, - OnLoadParams, - Elements, - Connection, - Edge, - ElementId, - Node -} from '../../src'; -import Sidebar from './Sidebar'; - -import './dnd.css'; -import { defineComponent, ref } from 'vue'; - -const initialElements = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }]; - -const onDragOver = (event: DragEvent) => { - event.preventDefault(); - if (event.dataTransfer) { - event.dataTransfer.dropEffect = 'move'; - } -}; - -let id = 0; -const getId = (): ElementId => `dndnode_${id++}`; - -const DnDFlow = defineComponent({ - setup() { - const revueFlowInstance = ref(); - const elements = ref(initialElements); - - const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value)); - const onElementsRemove = (elementsToRemove: Elements) => (elements.value = removeElements(elementsToRemove, elements.value)); - const onLoad = (instance: OnLoadParams) => (revueFlowInstance.value = instance); - - const onDrop = (event: DragEvent) => { - event.preventDefault(); - - if (revueFlowInstance.value) { - console.log(event.dataTransfer?.getData('application/revueflow')); - const type = event.dataTransfer?.getData('application/revueflow'); - const position = revueFlowInstance.value.project({ x: event.clientX, y: event.clientY - 40 }); - const newNode: Node = { - id: getId(), - type, - position, - data: { label: `${type} node` } - }; - - elements.value.push(newNode); - } - }; - - return () => ( - - - - - - - - - ); - } -}); - -export default DnDFlow; diff --git a/examples/router.ts b/examples/router.ts index 76e8959c..efbe90b0 100644 --- a/examples/router.ts +++ b/examples/router.ts @@ -3,19 +3,19 @@ import { createRouter, createWebHashHistory } from 'vue-router'; export const routes = [ { path: '/basic', - component: () => import('./Basic') + component: () => import('./Basic/Basic.vue') }, { path: '/custom-connectionline', - component: () => import('./CustomConnectionLine') + component: () => import('./CustomConnectionLine/CustomConnectionLine.vue') }, { path: '/custom-node', - component: () => import('./CustomNode') + component: () => import('./CustomNode/CustomNode.vue') }, { path: '/drag-n-drop', - component: () => import('./DragNDrop') + component: () => import('./DragNDrop/DnD.vue') } ]; diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index d8d1e0a9..53709ecc 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,6 +1,6 @@ import { defineComponent, HTMLAttributes, inject, onMounted, PropType, ref } from 'vue'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; -import { FitViewParams, RevueFlowStore } from '../../types'; +import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '../../types'; import PlusIcon from '../../../assets/icons/plus.svg'; import MinusIcon from '../../../assets/icons/minus.svg'; import Fitview from '../../../assets/icons/fitview.svg'; @@ -89,27 +89,28 @@ const Controls = defineComponent({ setup(props, { slots }) { const store = inject('store')!; const isVisible = ref(false); - const zoomHelper = ref(useZoomPanHelper()); + const zoomHelper = ref(); + const { onReady } = useZoomPanHelper(store); - onMounted(() => { - zoomHelper.value = useZoomPanHelper(); + onReady((helper) => { + zoomHelper.value = helper; }); const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable; const mapClasses = ['revue-flow__controls']; const onZoomInHandler = () => { - zoomHelper.value.zoomIn?.(); + zoomHelper.value?.zoomIn?.(); props.onZoomIn?.(); }; const onZoomOutHandler = () => { - zoomHelper.value.zoomOut?.(); + zoomHelper.value?.zoomOut?.(); props.onZoomOut?.(); }; const onFitViewHandler = () => { - zoomHelper.value.fitView?.(props.fitViewParams); + zoomHelper.value?.fitView?.(props.fitViewParams); props.onFitView?.(); }; diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index bab08dfc..b792b25a 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -4,14 +4,13 @@ import { ElementId, XYPosition, OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, ConnectionMode, SetConnectionId, Connection, - HandleType + HandleType, + RevueFlowStore } from '../../types'; +import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'; export type ValidConnectionFunc = (connection: Connection) => boolean; export type SetSourceIdFunc = (params: SetConnectionId) => void; @@ -85,19 +84,19 @@ function resetRecentHandle(hoveredHandle: Element): void { export function onMouseDown( event: MouseEvent, + store: RevueFlowStore, + hooks: { + connectStart: RevueFlowHooks['connectStart']; + connectStop: RevueFlowHooks['connectStop']; + connectEnd: RevueFlowHooks['connectEnd']; + }, handleId: ElementId | null, nodeId: ElementId, - setConnectionNodeId: SetSourceIdFunc, - setPosition: SetPosition, onConnect: OnConnectFunc, isTarget: boolean, isValidConnection: ValidConnectionFunc, - connectionMode: ConnectionMode, elementEdgeUpdaterType?: HandleType, - onEdgeUpdateEnd?: (evt: MouseEvent) => void, - onConnectStart?: OnConnectStartFunc, - onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc + onEdgeUpdateEnd?: (evt: MouseEvent) => void ): void { const revueFlowNode = (event.target as Element).closest('.revue-flow'); // when revue-flow is used inside a shadow root we can't use document @@ -119,23 +118,23 @@ export function onMouseDown( const containerBounds = revueFlowNode.getBoundingClientRect(); let recentHoveredHandle: Element; - setPosition({ - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top - }); + store.connectionPosition.x = event.clientX - containerBounds.left; + store.connectionPosition.y = event.clientY - containerBounds.top; - setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType }); - onConnectStart?.(event, { nodeId, handleId, handleType }); + store.setConnectionNodeId({ + connectionNodeId: nodeId, + connectionHandleId: handleId, + connectionHandleType: handleType + }); + hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } }); function onMouseMove(event: MouseEvent) { - setPosition({ - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top - }); + store.connectionPosition.x = event.clientX - containerBounds.left; + store.connectionPosition.y = event.clientY - containerBounds.top; const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid( event, - connectionMode, + store.connectionMode, isTarget, nodeId, handleId, @@ -159,7 +158,7 @@ export function onMouseDown( function onMouseUp(event: MouseEvent) { const { connection, isValid } = checkElementBelowIsValid( event, - connectionMode, + store.connectionMode, isTarget, nodeId, handleId, @@ -167,20 +166,20 @@ export function onMouseDown( doc ); - onConnectStop?.(event); + hooks.connectStop.trigger(event); if (isValid) { onConnect?.(connection); } - onConnectEnd?.(event); + hooks.connectEnd.trigger(event); if (elementEdgeUpdaterType && onEdgeUpdateEnd) { onEdgeUpdateEnd(event); } resetRecentHandle(recentHoveredHandle); - setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null }); + store.setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null }); doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 28e5ce7b..3c149ae7 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,6 +1,7 @@ import { Connection, ElementId, Position, RevueFlowStore } from '../../types'; import { onMouseDown, ValidConnectionFunc } from './handler'; import { computed, defineComponent, inject, PropType } from 'vue'; +import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'; const alwaysValid = () => true; @@ -39,6 +40,7 @@ const Handle = defineComponent({ }, setup(props, { slots }) { const store = inject('store')!; + const hooks = inject('hooks')!; const nodeId = inject('NodeIdContext') as ElementId; const isTarget = computed(() => props.type === 'target'); const onConnect = computed(() => store.onConnect); @@ -51,19 +53,15 @@ const Handle = defineComponent({ const onMouseDownHandler = (event: MouseEvent) => { onMouseDown( event, + store, + hooks, props.id as string, nodeId, - store.setConnectionNodeId, - store.setConnectionPosition, onConnectExtended, isTarget.value, props.isValidConnection, - store.connectionMode, undefined, - undefined, - store.onConnectStart, - store.onConnectStop, - store.onConnectEnd + undefined ); }; diff --git a/src/container/FlowRenderer/FlowRenderer.vue b/src/container/FlowRenderer/FlowRenderer.vue new file mode 100644 index 00000000..13566cfd --- /dev/null +++ b/src/container/FlowRenderer/FlowRenderer.vue @@ -0,0 +1,225 @@ + + + + + + + + + diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 353dc824..27d17607 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -155,10 +155,11 @@ const FlowRenderer = defineComponent({ } }, setup(props, { slots }) { - const store = inject('store'); + const store = inject('store')!; const keyPressed = useKeyPress(props.selectionKeyCode); useGlobalKeyHandler({ + store, onElementsRemove: props.onElementsRemove, deleteKeyCode: props.deleteKeyCode as string, multiSelectionKeyCode: props.multiSelectionKeyCode as string diff --git a/src/container/GraphView/GraphView.vue b/src/container/GraphView/GraphView.vue new file mode 100644 index 00000000..8b4d0d10 --- /dev/null +++ b/src/container/GraphView/GraphView.vue @@ -0,0 +1,480 @@ + + + + + + + diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index c797a79e..b5127753 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, inject, onBeforeMount, onMounted, ref, PropType, CSSProperties } from 'vue'; +import { defineComponent, inject, onBeforeMount, onMounted, PropType, CSSProperties } from 'vue'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -342,13 +342,11 @@ const GraphView = defineComponent({ } }, setup(props) { - const store = inject('store'); - const isInitialized = ref(false); + const store = inject('store')!; - onMounted(() => { - const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); - - if (!isInitialized.value && initialized) { + const { onReady } = useZoomPanHelper(store); + onReady(({ zoomIn, zoomOut, zoomTo, transform, fitView, initialized }) => { + if (initialized) { if (props.onLoad && store) { props.onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), @@ -361,58 +359,32 @@ const GraphView = defineComponent({ toObject: onLoadToObject(store) }); } - isInitialized.value = true; } }); onMounted(() => { if (typeof props.nodesConnectable !== 'undefined') { - store?.setNodesConnectable(!!props.nodesConnectable); + store.nodesConnectable = !!props.nodesConnectable; } if (typeof props.elementsSelectable !== 'undefined') { - store?.setElementsSelectable(!!props.elementsSelectable); + store.elementsSelectable = !!props.elementsSelectable; + } + if (typeof props.nodesDraggable !== 'undefined') { + store.nodesDraggable = !!props.nodesDraggable; } if (typeof props.connectionMode !== 'undefined') { - store?.setConnectionMode(props.connectionMode); + store.connectionMode = props.connectionMode; } }); onBeforeMount(() => { - if (props.onConnect) { - store?.setOnConnect(props.onConnect); - } - - if (props.onConnectStart) { - store?.setOnConnectStart(props.onConnectStart); - } - - if (props.onConnectStop) { - store?.setOnConnectStop(props.onConnectStop); - } - - if (props.onConnectEnd) { - store?.setOnConnectEnd(props.onConnectEnd); - } - if (typeof props.snapToGrid !== 'undefined') { - store?.setSnapToGrid(props.snapToGrid); + store.snapToGrid = props.snapToGrid; } if (typeof props.snapGrid !== 'undefined') { - store?.setSnapGrid(props.snapGrid); - } - - if (typeof props.nodesDraggable !== 'undefined') { - store?.setNodesDraggable(!!props.nodesDraggable); - } - - if (typeof props.nodesConnectable !== 'undefined') { - store?.setNodesConnectable(!!props.nodesConnectable); - } - - if (typeof props.elementsSelectable !== 'undefined') { - store?.setElementsSelectable(!!props.elementsSelectable); + store.snapGrid = props.snapGrid; } if (typeof props.minZoom !== 'undefined') { diff --git a/src/container/RevueFlow/RevueFlow.vue b/src/container/RevueFlow/RevueFlow.vue new file mode 100644 index 00000000..ef75fe13 --- /dev/null +++ b/src/container/RevueFlow/RevueFlow.vue @@ -0,0 +1,491 @@ + + + + + + + diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index d9fff78e..03cdc7f9 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -198,7 +198,7 @@ const ZoomPane = defineComponent({ store.d3Zoom.on('zoom', null); } else { store.d3Zoom.on('zoom', (event: any) => { - store.updateTransform([event.transform.x, event.transform.y, event.transform.k]); + store.transform = [event.transform.x, event.transform.y, event.transform.k]; if (props.onMove) { const flowTransform = eventToFlowTransform(event.transform); diff --git a/src/hooks/RevueFlowHooks.ts b/src/hooks/RevueFlowHooks.ts new file mode 100644 index 00000000..65cc9062 --- /dev/null +++ b/src/hooks/RevueFlowHooks.ts @@ -0,0 +1,96 @@ +// global hooks +import { Connection, Edge, Elements, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '../types'; +import { createEventHook, EventHook } from '@vueuse/core'; +import { SetupContext } from 'vue'; + +export interface RevueFlowHooks { + elementClick: EventHook<{ event: MouseEvent; element: Node | Edge }>; + elementsRemove: EventHook; + nodeDoubleClick: EventHook<{ event: MouseEvent; node: Node }>; + nodeMouseEnter: EventHook<{ event: MouseEvent; node: Node }>; + nodeMouseMove: EventHook<{ event: MouseEvent; node: Node }>; + nodeMouseLeave: EventHook<{ event: MouseEvent; node: Node }>; + nodeContextMenu: EventHook<{ event: MouseEvent; node: Node }>; + nodeDragStart: EventHook<{ event: MouseEvent; node: Node }>; + nodeDrag: EventHook<{ event: MouseEvent; node: Node }>; + nodeDragStop: EventHook<{ event: MouseEvent; node: Node }>; + connect: EventHook; + connectStart: EventHook<{ + event: MouseEvent; + params: OnConnectStartParams; + }>; + connectStop: EventHook; + connectEnd: EventHook; + load: EventHook; + move: EventHook; + moveStart: EventHook; + moveEnd: EventHook; + selectionChange: EventHook; + selectionDragStart: EventHook<{ event: MouseEvent; nodes: Node[] }>; + selectionDrag: EventHook<{ event: MouseEvent; nodes: Node[] }>; + selectionDragStop: EventHook<{ event: MouseEvent; nodes: Node[] }>; + selectionContextMenu: EventHook<{ event: MouseEvent; nodes: Node[] }>; + paneScroll: EventHook; + paneClick: EventHook; + paneContextMenu: EventHook; + edgeUpdate: EventHook<{ oldEdge: Edge; newConnection: Connection }>; + edgeContextMenu: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeMouseEnter: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeMouseMove: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeMouseLeave: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeDoubleClick: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeUpdateStart: EventHook<{ event: MouseEvent; edge: Edge }>; + edgeUpdateEnd: EventHook<{ event: MouseEvent; edge: Edge }>; +} + +export const useRevueFlow = (emit: SetupContext['emit']): RevueFlowHooks => { + const eventHooks: RevueFlowHooks = { + elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(), + elementsRemove: createEventHook(), + nodeDoubleClick: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeMouseEnter: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeMouseMove: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeMouseLeave: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeContextMenu: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeDragStart: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeDrag: createEventHook<{ event: MouseEvent; node: Node }>(), + nodeDragStop: createEventHook<{ event: MouseEvent; node: Node }>(), + connect: createEventHook(), + connectStart: createEventHook<{ + event: MouseEvent; + params: OnConnectStartParams; + }>(), + connectStop: createEventHook(), + connectEnd: createEventHook(), + load: createEventHook(), + move: createEventHook(), + moveStart: createEventHook(), + moveEnd: createEventHook(), + selectionChange: createEventHook(), + selectionDragStart: createEventHook<{ event: MouseEvent; nodes: Node[] }>(), + selectionDrag: createEventHook<{ event: MouseEvent; nodes: Node[] }>(), + selectionDragStop: createEventHook<{ event: MouseEvent; nodes: Node[] }>(), + selectionContextMenu: createEventHook<{ event: MouseEvent; nodes: Node[] }>(), + paneScroll: createEventHook(), + paneClick: createEventHook(), + paneContextMenu: createEventHook(), + edgeUpdate: createEventHook<{ oldEdge: Edge; newConnection: Connection }>(), + edgeContextMenu: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeMouseEnter: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeMouseMove: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeMouseLeave: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeDoubleClick: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeUpdateStart: createEventHook<{ event: MouseEvent; edge: Edge }>(), + edgeUpdateEnd: createEventHook<{ event: MouseEvent; edge: Edge }>() + }; + + for (const [key, value] of Object.entries(eventHooks)) { + value.on((data: any) => { + emit(key, data); + }); + } + + return { + ...eventHooks + }; +}; diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 86d904ed..f0248a9c 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,21 +1,20 @@ import useKeyPress from './useKeyPress'; import { isNode, getConnectedEdges } from '../utils/graph'; import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore, Edge } from '../types'; -import { computed, inject, watch } from 'vue'; +import { computed, watch } from 'vue'; interface HookParams { + store: RevueFlowStore; deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; onElementsRemove?: (elements: Elements) => void; } -export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { - const store = inject('store'); - +export default ({ store, deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); - const selectedElements = computed(() => store?.selectedElements || []); - const edges = computed(() => store?.edges); + const selectedElements = computed(() => store.selectedElements || []); + const edges = computed(() => store.edges); watch(selectedElements, () => { if (onElementsRemove && deleteKeyPressed.value && selectedElements.value.length > 0) { @@ -27,12 +26,12 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook ); onElementsRemove(Array.from(elementsToRemove.values())); - store?.unsetNodesSelection(); - store?.resetSelectedElements(); + store.unsetNodesSelection(); + store.resetSelectedElements(); } }); watch(multiSelectionKeyPressed, () => { - store?.setMultiSelectionActive(multiSelectionKeyPressed.value); + store.multiSelectionActive = multiSelectionKeyPressed.value; }); }; diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 86f2ea33..95bec9fa 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,68 +1,85 @@ import { zoomIdentity } from 'd3-zoom'; +import { computed } from 'vue'; +import { createEventHook, until } from '@vueuse/core'; +import { Selection, ZoomBehavior } from 'd3'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, RevueFlowStore } from '../types'; -import { inject } from 'vue'; const DEFAULT_PADDING = 0.1; -const useZoomPanHelper = (): ZoomPanHelperFunctions => { - const store = inject('store')!; - const zoomPanHelperFunctions = () => { - return { - zoomIn: () => store.d3Zoom?.scaleBy(store.d3Selection as any, 1.2), - zoomOut: () => store.d3Zoom?.scaleBy(store.d3Selection as any, 1 / 1.2), - zoomTo: (zoomLevel: number) => store.d3Zoom?.scaleTo(store.d3Selection as any, zoomLevel), - transform: (transform: FlowTransform) => { - const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); +const useZoomPanHelper = (store: RevueFlowStore) => { + const zoomPanHelperEvent = createEventHook(); + const d3Zoom = computed(() => store.d3Zoom); + const d3Selection = computed(() => store.d3Selection); - store.d3Zoom?.transform(store.d3Selection as any, nextTransform); - }, - fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => { - const { nodes, width, height, minZoom, maxZoom } = store; + const createZoomPanHelper = ( + d3Zoom: ZoomBehavior, + d3Selection: Selection + ): ZoomPanHelperFunctions => ({ + zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2), + zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2), + zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel), + transform: (transform: FlowTransform) => { + const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); - if (!nodes.length) { - return; - } + d3Zoom.transform(d3Selection, nextTransform); + }, + fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => { + const { nodes, width, height, minZoom, maxZoom } = store; - const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden)); - const [x, y, zoom] = getTransformForBounds( - bounds, - width, - height, - options.minZoom || minZoom, - options.maxZoom || maxZoom, - options.padding || DEFAULT_PADDING - ); - const transform = zoomIdentity.translate(x, y).scale(zoom); + if (!nodes.length) { + return; + } - store.d3Zoom?.transform(store.d3Selection as any, transform); - }, - setCenter: (x: number, y: number, zoom?: number) => { - const { width, height, maxZoom } = store; + const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden)); + const [x, y, zoom] = getTransformForBounds( + bounds, + width, + height, + options.minZoom || minZoom, + options.maxZoom || maxZoom, + options.padding || DEFAULT_PADDING + ); + const transform = zoomIdentity.translate(x, y).scale(zoom); - const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom; - const centerX = width / 2 - x * nextZoom; - const centerY = height / 2 - y * nextZoom; - const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom); + d3Zoom?.transform(d3Selection, transform); + }, + setCenter: (x: number, y: number, zoom?: number) => { + const { width, height, maxZoom } = store; - store.d3Zoom?.transform(store.d3Selection as any, transform); - }, - fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => { - const { width, height, minZoom, maxZoom } = store; - const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding); - const transform = zoomIdentity.translate(x, y).scale(zoom); + const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom; + const centerX = width / 2 - x * nextZoom; + const centerY = height / 2 - y * nextZoom; + const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom); - store.d3Zoom?.transform(store.d3Selection as any, transform); - }, - project: (position: XYPosition) => { - const { transform, snapToGrid, snapGrid } = store; - return pointToRendererPoint(position, transform, snapToGrid, snapGrid); - }, - initialized: true - }; + d3Zoom.transform(d3Selection, transform); + }, + fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => { + const { width, height, minZoom, maxZoom } = store; + const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding); + const transform = zoomIdentity.translate(x, y).scale(zoom); + + d3Zoom.transform(d3Selection, transform); + }, + project: (position: XYPosition) => { + const { transform, snapToGrid, snapGrid } = store; + return pointToRendererPoint(position, transform, snapToGrid, snapGrid); + }, + initialized: true + }); + + until(d3Zoom) + .toBeTruthy() + .then(() => { + if (d3Zoom.value && d3Selection.value) { + zoomPanHelperEvent.trigger(createZoomPanHelper(d3Zoom.value, d3Selection.value)); + } else { + throw Error('D3 could not be initialized.'); + } + }); + return { + onReady: zoomPanHelperEvent.on }; - - return zoomPanHelperFunctions(); }; export default useZoomPanHelper; diff --git a/src/index.ts b/src/index.ts index e45a2492..d832a4fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export { default as default } from './container/RevueFlow'; +export { default as default } from './container/RevueFlow/RevueFlow.vue'; export { default as Handle } from './components/Handle'; export { default as EdgeText } from './components/Edges/EdgeText'; export { getBezierPath } from './components/Edges/BezierEdge'; diff --git a/src/store/configure-store.ts b/src/store/configure-store.ts index 5d61f5ad..514094ad 100644 --- a/src/store/configure-store.ts +++ b/src/store/configure-store.ts @@ -21,18 +21,6 @@ export default function configureStore( state: () => preloadedState, getters: {}, actions: { - setOnConnect(onConnect) { - this.onConnect = onConnect; - }, - setOnConnectStart(onConnectStart) { - this.onConnectStart = onConnectStart; - }, - setOnConnectEnd(onConnectEnd) { - this.onConnectEnd = onConnectEnd; - }, - setOnConnectStop(onConnectStop) { - this.onConnectStop = onConnectStop; - }, setElements(elements) { const propElements = elements; const nextElements: NextElements = { @@ -252,46 +240,19 @@ export default function configureStore( unsetNodesSelection() { this.nodesSelectionActive = false; }, - updateTransform(transform) { - this.transform = transform; - }, updateSize(size) { this.height = size.height; this.width = size.width; }, - setConnectionPosition(connectionPosition) { - this.connectionPosition = connectionPosition; - }, setConnectionNodeId(payload) { this.connectionNodeId = payload.connectionNodeId; this.connectionHandleId = payload.connectionHandleId; this.connectionHandleType = payload.connectionHandleType; }, - setSnapToGrid(snapToGrid) { - this.snapToGrid = snapToGrid; - }, - setSnapGrid(snapGrid) { - this.snapGrid = snapGrid; - }, setInteractive(isInteractive) { this.nodesDraggable = isInteractive; this.nodesConnectable = isInteractive; this.elementsSelectable = isInteractive; - }, - setNodesDraggable(nodesDraggable) { - this.nodesDraggable = nodesDraggable; - }, - setNodesConnectable(nodesConnectable) { - this.nodesConnectable = nodesConnectable; - }, - setElementsSelectable(elementsSelectable) { - this.elementsSelectable = elementsSelectable; - }, - setMultiSelectionActive(multiSelectionActive) { - this.multiSelectionActive = multiSelectionActive; - }, - setConnectionMode(connectionMode) { - this.connectionMode = connectionMode; } } }); diff --git a/src/types/index.ts b/src/types/index.ts index dc8f5521..f7cd42a7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -870,10 +870,6 @@ export interface RevueFlowState { } export type RevueFlowActions = { - setOnConnect: (onConnect: OnConnectFunc) => void; - setOnConnectStart: (onConnectStart: OnConnectStartFunc) => void; - setOnConnectStop: (onConnectStop: OnConnectStopFunc) => void; - setOnConnectEnd: (onConnectEnd: OnConnectEndFunc) => void; setElements: (elements: Elements) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodePos: (payload: NodePosUpdate) => void; @@ -889,18 +885,9 @@ export type RevueFlowActions = { setNodeExtent: (nodeExtent: NodeExtent) => void; resetSelectedElements: () => void; unsetNodesSelection: () => void; - updateTransform: (transform: Transform) => void; updateSize: (size: Dimensions) => void; - setConnectionPosition: (connectionPosition: XYPosition) => void; setConnectionNodeId: (payload: SetConnectionId) => void; - setSnapToGrid: (snapToGrid: boolean) => void; - setSnapGrid: (snapGrid: SnapGrid) => void; setInteractive: (isInteractive: boolean) => void; - setNodesDraggable: (nodesDraggable: boolean) => void; - setNodesConnectable: (nodesConnectable: boolean) => void; - setElementsSelectable: (elementsSelectable: boolean) => void; - setMultiSelectionActive: (multiSelectionActive: boolean) => void; - setConnectionMode: (connectionMode: ConnectionMode) => void; }; export type RevueFlowStore = Store; @@ -935,6 +922,14 @@ export interface RevueFlowProps extends Omit { onPaneScroll?: (event?: WheelEvent) => void; onPaneClick?: (event: MouseEvent) => void; onPaneContextMenu?: (event: MouseEvent) => void; + onEdgeUpdate?: OnEdgeUpdateFunc; + onEdgeContextMenu?: (event: MouseEvent, edge: Edge) => void; + onEdgeMouseEnter?: (event: MouseEvent, edge: Edge) => void; + onEdgeMouseMove?: (event: MouseEvent, edge: Edge) => void; + onEdgeMouseLeave?: (event: MouseEvent, edge: Edge) => void; + onEdgeDoubleClick?: (event: MouseEvent, edge: Edge) => void; + onEdgeUpdateStart?: (event: MouseEvent, edge: Edge) => void; + onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; nodeTypes?: NodeTypesType; edgeTypes?: EdgeTypesType; connectionMode?: ConnectionMode; @@ -967,14 +962,6 @@ export interface RevueFlowProps extends Omit { panOnScrollSpeed?: number; panOnScrollMode?: PanOnScrollMode; zoomOnDoubleClick?: boolean; - onEdgeUpdate?: OnEdgeUpdateFunc; - onEdgeContextMenu?: (event: MouseEvent, edge: Edge) => void; - onEdgeMouseEnter?: (event: MouseEvent, edge: Edge) => void; - onEdgeMouseMove?: (event: MouseEvent, edge: Edge) => void; - onEdgeMouseLeave?: (event: MouseEvent, edge: Edge) => void; - onEdgeDoubleClick?: (event: MouseEvent, edge: Edge) => void; - onEdgeUpdateStart?: (event: MouseEvent, edge: Edge) => void; - onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; nodeTypesId?: string; edgeTypesId?: string;