diff --git a/examples/react/src/examples/DefaultNodes/index.tsx b/examples/react/src/examples/DefaultNodes/index.tsx index 46ef8e0c..354fa994 100644 --- a/examples/react/src/examples/DefaultNodes/index.tsx +++ b/examples/react/src/examples/DefaultNodes/index.tsx @@ -54,26 +54,24 @@ const DefaultNodes = () => { const updateNodePositions = () => { instance.setNodes((nodes) => - nodes.map((node) => { - node.position = { + nodes.map((node) => ({ + ...node, + position: { x: Math.random() * 400, y: Math.random() * 400, - }; - - return node; - }) + }, + })) ); }; const updateEdgeColors = () => { instance.setEdges((edges) => - edges.map((edge) => { - edge.style = { + edges.map((edge) => ({ + ...edge, + style: { stroke: '#ff5050', - }; - - return edge; - }) + }, + })) ); }; diff --git a/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx index 5d467514..e0897509 100644 --- a/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx +++ b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx @@ -44,10 +44,10 @@ export default function NodeInspector() {
{nodes.map((node) => { - const x = node.computed?.positionAbsolute?.x || 0; - const y = node.computed?.positionAbsolute?.y || 0; - const width = node.computed?.width || 0; - const height = node.computed?.height || 0; + const x = node?.position?.x || 0; + const y = node?.position?.y || 0; + const width = node.measured?.width || 0; + const height = node.measured?.height || 0; return ( { + const { addNodes } = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => { console.log('initialized', initialized); }, [initialized]); + const addNode = () => + addNodes({ + id: `${Math.random()}`, + data: { label: 'new node' }, + position: { x: Math.random() * 400, y: Math.random() * 400 }, + }); + return ( { > + + + ); }; diff --git a/examples/svelte/src/generic-tests/nodes/general.ts b/examples/svelte/src/generic-tests/nodes/general.ts index 4e5abc91..60757953 100644 --- a/examples/svelte/src/generic-tests/nodes/general.ts +++ b/examples/svelte/src/generic-tests/nodes/general.ts @@ -3,6 +3,7 @@ import DragHandleNode from './components/DragHandleNode.svelte'; export default { flowProps: { fitView: true, + nodeDragThreshold: 0, nodeTypes: { DragHandleNode }, diff --git a/examples/svelte/src/routes/examples/custom-connection-line/CustomNode.svelte b/examples/svelte/src/routes/examples/custom-connection-line/CustomNode.svelte index 0f4cc6bd..6507ce51 100644 --- a/examples/svelte/src/routes/examples/custom-connection-line/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/custom-connection-line/CustomNode.svelte @@ -1,34 +1,35 @@
-
Node
- - - +
Node
+ + +
diff --git a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte index e22701e3..47a33534 100644 --- a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte @@ -6,6 +6,8 @@ export let data: $$Props['data']; + $$restProps; + const { colorStore } = data; diff --git a/examples/svelte/src/routes/examples/edges/ButtonEdge.svelte b/examples/svelte/src/routes/examples/edges/ButtonEdge.svelte index 235aab98..ee582226 100644 --- a/examples/svelte/src/routes/examples/edges/ButtonEdge.svelte +++ b/examples/svelte/src/routes/examples/edges/ButtonEdge.svelte @@ -4,27 +4,18 @@ type $$Props = EdgeProps; export let id: $$Props['id'] = ''; - export let source: $$Props['source'] = ''; - export let target: $$Props['target'] = ''; - export let animated: $$Props['animated'] = undefined; - export let selected: $$Props['selected'] = undefined; - export let label: $$Props['label'] = undefined; - export let labelStyle: $$Props['labelStyle'] = undefined; - export let data: $$Props['data'] = undefined; export let style: $$Props['style'] = undefined; - export let markerStart: $$Props['markerStart'] = undefined; export let markerEnd: $$Props['markerEnd'] = undefined; - export let interactionWidth: $$Props['interactionWidth'] = undefined; export let sourceX: $$Props['sourceX']; export let sourceY: $$Props['sourceY']; export let sourcePosition: $$Props['sourcePosition']; - export let sourceHandleId: $$Props['sourceHandleId'] = undefined; export let targetX: $$Props['targetX']; export let targetY: $$Props['targetY']; export let targetPosition: $$Props['targetPosition']; - export let targetHandleId: $$Props['targetHandleId'] = undefined; + + $$restProps; $: [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -34,18 +25,6 @@ targetY, targetPosition }); - - source; - target; - animated; - selected; - data; - label; - labelStyle; - markerStart; - interactionWidth; - sourceHandleId; - targetHandleId; diff --git a/examples/svelte/src/routes/examples/edges/CustomBezierEdge.svelte b/examples/svelte/src/routes/examples/edges/CustomBezierEdge.svelte index 48d86c3d..960d3e6e 100644 --- a/examples/svelte/src/routes/examples/edges/CustomBezierEdge.svelte +++ b/examples/svelte/src/routes/examples/edges/CustomBezierEdge.svelte @@ -3,14 +3,8 @@ type $$Props = EdgeProps; - export let id: $$Props['id'] = ''; - export let source: $$Props['source'] = ''; - export let target: $$Props['target'] = ''; - export let animated: $$Props['animated'] = undefined; - export let selected: $$Props['selected'] = undefined; export let label: $$Props['label'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined; - export let data: $$Props['data'] = undefined; export let style: $$Props['style'] = undefined; export let markerStart: $$Props['markerStart'] = undefined; export let markerEnd: $$Props['markerEnd'] = undefined; @@ -19,23 +13,12 @@ export let sourceX: $$Props['sourceX']; export let sourceY: $$Props['sourceY']; export let sourcePosition: $$Props['sourcePosition']; - export let sourceHandleId: $$Props['sourceHandleId'] = undefined; export let targetX: $$Props['targetX']; export let targetY: $$Props['targetY']; export let targetPosition: $$Props['targetPosition']; - export let targetHandleId: $$Props['targetHandleId'] = undefined; - id; - source; - target; - animated; - selected; - data; - sourcePosition; - targetPosition; - sourceHandleId; - targetHandleId; + $$restProps;
diff --git a/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte b/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte index 7c1e026d..d2958969 100644 --- a/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte +++ b/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte @@ -21,34 +21,7 @@ console.log('disconnect source', connection); } - export let data: $$Props['data']; - export let targetPosition: $$Props['targetPosition'] = Position.Top; - export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom; - export let width: $$Props['width'] = undefined; - export let height: $$Props['height'] = undefined; - export let selected: $$Props['selected'] = undefined; - export let type: $$Props['type'] = undefined; - export let zIndex: $$Props['zIndex'] = undefined; - export let dragging: $$Props['dragging'] = false; - export let dragHandle: $$Props['dragHandle'] = undefined; - export let positionAbsolute: $$Props['positionAbsolute'] = { - x: 0, - y: 0 - }; - export let isConnectable: $$Props['isConnectable'] = undefined; - - data; - targetPosition; - sourcePosition; - width; - height; - selected; - type; - zIndex; - dragging; - dragHandle; - positionAbsolute; - isConnectable; + $$restProps;
diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index 8e08174a..e03acf34 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -1,13 +1,14 @@ import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte'; + import type { ResizeNode } from './types'; - type $$Props = NodeProps; + type $$Props = NodeProps; - export let data: $$Props['data'] = undefined; + export let data: $$Props['data']; export let selected: $$Props['selected'] = undefined; diff --git a/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte b/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte index 4942e511..5caea508 100644 --- a/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte @@ -1,9 +1,10 @@ - - - - - - - ); -} - -export default ResizeIcon; diff --git a/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte b/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte index 65ccae15..5e173508 100644 --- a/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte @@ -1,9 +1,10 @@ ; diff --git a/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte b/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte index af2dabaa..99b00f7c 100644 --- a/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte @@ -2,6 +2,7 @@ import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte'; type $$Props = NodeProps; + $$restProps; export let data: $$Props['data']; diff --git a/examples/svelte/src/routes/examples/overview/+page.svelte b/examples/svelte/src/routes/examples/overview/+page.svelte index 301730f6..92a353bb 100644 --- a/examples/svelte/src/routes/examples/overview/+page.svelte +++ b/examples/svelte/src/routes/examples/overview/+page.svelte @@ -13,8 +13,8 @@ type Node, type Edge, ConnectionMode, - useSvelteFlow, - ControlButton + ControlButton, + type FitViewOptions } from '@xyflow/svelte'; import CustomNode from './CustomNode.svelte'; @@ -33,6 +33,11 @@ custom: CustomEdge }; + const fitViewOptions: FitViewOptions = { + padding: 0.2, + nodes: [{ id: '1' }, { id: '2' }] + }; + const nodes = writable([ { id: '1', @@ -191,7 +196,7 @@ attributionPosition={'top-center'} deleteKey={['Backspace', 'd']} > - + xy console.log('control button')} >log - import { Handle, Position, type NodeProps } from '@xyflow/svelte'; + import { Handle, Position, type BuiltInNode, type NodeProps } from '@xyflow/svelte'; - type $$Props = NodeProps; + type $$Props = NodeProps; + $$restProps; export let data: { label: string } = { label: 'Node' }; export let positionAbsoluteX: number = 0; diff --git a/examples/svelte/src/routes/examples/overview/CustomNodeDragHandle.svelte b/examples/svelte/src/routes/examples/overview/CustomNodeDragHandle.svelte index dcdddfc8..c087f7de 100644 --- a/examples/svelte/src/routes/examples/overview/CustomNodeDragHandle.svelte +++ b/examples/svelte/src/routes/examples/overview/CustomNodeDragHandle.svelte @@ -1,7 +1,8 @@ diff --git a/examples/svelte/src/routes/examples/subflows/DebugNode.svelte b/examples/svelte/src/routes/examples/subflows/DebugNode.svelte index 9676957e..ecacb06b 100644 --- a/examples/svelte/src/routes/examples/subflows/DebugNode.svelte +++ b/examples/svelte/src/routes/examples/subflows/DebugNode.svelte @@ -1,16 +1,19 @@
{id}
- x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex} + x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
diff --git a/examples/svelte/src/routes/examples/useupdatenodeinternals/CustomNode.svelte b/examples/svelte/src/routes/examples/useupdatenodeinternals/CustomNode.svelte index fbe9c8d7..89a73e1c 100644 --- a/examples/svelte/src/routes/examples/useupdatenodeinternals/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/useupdatenodeinternals/CustomNode.svelte @@ -4,6 +4,7 @@ type $$Props = NodeProps; export let id: $$Props['id']; + $$restProps; const updateNodeInternals = useUpdateNodeInternals(); diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 6685ad5b..5bbe2a56 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,15 @@ # @xyflow/react +## 12.0.0-next.17 + +- fix broken `defaultNodes` +- add string array to `UpdateNodeInternals` thanks @DenizUgur +- pinch zoom on windows +- drag for touch devices +- return user node in node event handlers +- cleanup `useReactFlow` +- export `KeyCode` and `Align` type + ## 12.0.0-next.16 ## Patch changes diff --git a/packages/react/package.json b/packages/react/package.json index 3b99d721..1cfc0aec 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.16", + "version": "12.0.0-next.17", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "keywords": [ "react", diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index dfbb7b93..b509824c 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -98,11 +98,21 @@ export function NodeWrapper({ }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; - const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined; - const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined; - const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined; - const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined; - const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined; + const onMouseEnterHandler = onMouseEnter + ? (event: MouseEvent) => onMouseEnter(event, { ...internals.userNode }) + : undefined; + const onMouseMoveHandler = onMouseMove + ? (event: MouseEvent) => onMouseMove(event, { ...internals.userNode }) + : undefined; + const onMouseLeaveHandler = onMouseLeave + ? (event: MouseEvent) => onMouseLeave(event, { ...internals.userNode }) + : undefined; + const onContextMenuHandler = onContextMenu + ? (event: MouseEvent) => onContextMenu(event, { ...internals.userNode }) + : undefined; + const onDoubleClickHandler = onDoubleClick + ? (event: MouseEvent) => onDoubleClick(event, { ...internals.userNode }) + : undefined; const onSelectNodeHandler = (event: MouseEvent) => { const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); @@ -118,7 +128,7 @@ export function NodeWrapper({ } if (onClick) { - onClick(event, { ...node }); + onClick(event, { ...internals.userNode }); } }; diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index ae0a84d1..fda3a8c9 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -5,7 +5,7 @@ import { useRef, useEffect, type MouseEvent, type KeyboardEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { getInternalNodesBounds } from '@xyflow/system'; +import { getInternalNodesBounds, isNumeric } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { useDrag } from '../../hooks/useDrag'; @@ -26,8 +26,8 @@ const selector = (s: ReactFlowState) => { }); return { - width, - height, + width: isNumeric(width) ? width : null, + height: isNumeric(height) ? height : null, userSelectionActive: s.userSelectionActive, transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]}) translate(${x}px,${y}px)`, }; diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 4ac0e50c..4f1404a4 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,18 +1,22 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { + EdgeRemoveChange, evaluateAbsolutePosition, getElementsToRemove, getOverlappingArea, isRectObject, + NodeRemoveChange, nodeToRect, type Rect, } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; -import { useStoreApi } from './useStore'; +import { useStore, useStoreApi } from './useStore'; import { useBatchContext } from '../components/BatchProvider'; -import { isNode } from '../utils'; -import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types'; +import { elementToRemoveChange, isNode } from '../utils'; +import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types'; + +const selector = (s: ReactFlowState) => !!s.panZoom; /** * Hook for accessing the ReactFlow instance. @@ -27,185 +31,40 @@ export function useReactFlow>( - () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], - [] - ); + const generalHelper = useMemo>(() => { + const getInternalNode: GeneralHelpers['getInternalNode'] = (id) => + store.getState().nodeLookup.get(id) as InternalNode; - const getInternalNode = useCallback>( - (id) => store.getState().nodeLookup.get(id) as InternalNode, - [] - ); - - const getNode = useCallback>( - (id) => getInternalNode(id)?.internals.userNode as NodeType, - [getInternalNode] - ); - - const getEdges = useCallback>(() => { - const { edges = [] } = store.getState(); - return edges.map((e) => ({ ...e })) as EdgeType[]; - }, []); - - const getEdge = useCallback>((id) => store.getState().edgeLookup.get(id) as EdgeType, []); - - const setNodes = useCallback>((payload) => { - batchContext.nodeQueue.push(payload as NodeType[]); - }, []); - - const setEdges = useCallback>((payload) => { - batchContext.edgeQueue.push(payload as EdgeType[]); - }, []); - - const addNodes = useCallback>((payload) => { - const newNodes = Array.isArray(payload) ? payload : [payload]; - batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]); - }, []); - - const addEdges = useCallback>((payload) => { - const newEdges = Array.isArray(payload) ? payload : [payload]; - batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]); - }, []); - - const toObject = useCallback>(() => { - const { nodes = [], edges = [], transform } = store.getState(); - const [x, y, zoom] = transform; - return { - nodes: nodes.map((n) => ({ ...n })) as NodeType[], - edges: edges.map((e) => ({ ...e })) as EdgeType[], - viewport: { - x, - y, - zoom, - }, - }; - }, []); - - const deleteElements = useCallback( - async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => { - const { - nodes, - edges, - hasDefaultNodes, - hasDefaultEdges, - onNodesDelete, - onEdgesDelete, - onNodesChange, - onEdgesChange, - onDelete, - onBeforeDelete, - } = store.getState(); - const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({ - nodesToRemove, - edgesToRemove, - nodes, - edges, - onBeforeDelete, - }); - - const hasMatchingEdges = matchingEdges.length > 0; - const hasMatchingNodes = matchingNodes.length > 0; - - if (hasMatchingEdges) { - if (hasDefaultEdges) { - const nextEdges = edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)); - store.getState().setEdges(nextEdges); - } - - onEdgesDelete?.(matchingEdges); - onEdgesChange?.( - matchingEdges.map((edge) => ({ - id: edge.id, - type: 'remove', - })) - ); - } - - if (hasMatchingNodes) { - if (hasDefaultNodes) { - const nextNodes = nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id)); - store.getState().setNodes(nextNodes); - } - - onNodesDelete?.(matchingNodes); - onNodesChange?.(matchingNodes.map((node) => ({ id: node.id, type: 'remove' }))); - } - - if (hasMatchingNodes || hasMatchingEdges) { - onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); - } - - return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; - }, - [] - ); - - const getNodeRect = useCallback((node: NodeType | { id: string }): Rect | null => { - const { nodeLookup, nodeOrigin } = store.getState(); - - const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id)!; - const position = nodeToUse.parentId - ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) - : nodeToUse.position; - - const nodeWithPosition = { - id: nodeToUse.id, - position, - width: nodeToUse.measured?.width ?? nodeToUse.width, - height: nodeToUse.measured?.height ?? nodeToUse.height, - data: nodeToUse.data, + const setNodes: GeneralHelpers['setNodes'] = (payload) => { + batchContext.nodeQueue.push(payload as NodeType[]); }; - return nodeToRect(nodeWithPosition); - }, []); + const getNodeRect = (node: NodeType | { id: string }): Rect | null => { + const { nodeLookup, nodeOrigin } = store.getState(); - const getIntersectingNodes = useCallback>( - (nodeOrRect, partially = true, nodes) => { - const isRect = isRectObject(nodeOrRect); - const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect); - const hasNodesOption = nodes !== undefined; + const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id)!; + const position = nodeToUse.parentId + ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) + : nodeToUse.position; - if (!nodeRect) { - return []; - } + const nodeWithPosition = { + id: nodeToUse.id, + position, + width: nodeToUse.measured?.width ?? nodeToUse.width, + height: nodeToUse.measured?.height ?? nodeToUse.height, + data: nodeToUse.data, + }; - return (nodes || store.getState().nodes).filter((n) => { - const internalNode = store.getState().nodeLookup.get(n.id); + return nodeToRect(nodeWithPosition); + }; - if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) { - return false; - } - - const currNodeRect = nodeToRect(hasNodesOption ? n : internalNode!); - const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); - const partiallyVisible = partially && overlappingArea > 0; - - return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; - }) as NodeType[]; - }, - [] - ); - - const isNodeIntersecting = useCallback>( - (nodeOrRect, area, partially = true) => { - const isRect = isRectObject(nodeOrRect); - const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect); - - if (!nodeRect) { - return false; - } - - const overlappingArea = getOverlappingArea(nodeRect, area); - const partiallyVisible = partially && overlappingArea > 0; - - return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; - }, - [] - ); - - const updateNode = useCallback>( - (id, nodeUpdate, options = { replace: false }) => { + const updateNode: GeneralHelpers['updateNode'] = ( + id, + nodeUpdate, + options = { replace: false } + ) => { setNodes((prevNodes) => prevNodes.map((node) => { if (node.id === id) { @@ -216,59 +75,139 @@ export function useReactFlow>( - (id, dataUpdate, options = { replace: false }) => { - updateNode( - id, - (node) => { - const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate; - return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } }; - }, - options - ); - }, - [updateNode] - ); + return { + getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], + getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType, + getInternalNode, + getEdges: () => { + const { edges = [] } = store.getState(); + return edges.map((e) => ({ ...e })) as EdgeType[]; + }, + getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType, + setNodes, + setEdges: (payload) => { + batchContext.edgeQueue.push(payload as EdgeType[]); + }, + addNodes: (payload) => { + const newNodes = Array.isArray(payload) ? payload : [payload]; + batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]); + }, + addEdges: (payload) => { + const newEdges = Array.isArray(payload) ? payload : [payload]; + batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]); + }, + toObject: () => { + const { nodes = [], edges = [], transform } = store.getState(); + const [x, y, zoom] = transform; + return { + nodes: nodes.map((n) => ({ ...n })) as NodeType[], + edges: edges.map((e) => ({ ...e })) as EdgeType[], + viewport: { + x, + y, + zoom, + }, + }; + }, + deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => { + const { + nodes, + edges, + onNodesDelete, + onEdgesDelete, + triggerNodeChanges, + triggerEdgeChanges, + onDelete, + onBeforeDelete, + } = store.getState(); + const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({ + nodesToRemove, + edgesToRemove, + nodes, + edges, + onBeforeDelete, + }); + + const hasMatchingEdges = matchingEdges.length > 0; + const hasMatchingNodes = matchingNodes.length > 0; + + if (hasMatchingEdges) { + const edgeChanges: EdgeRemoveChange[] = matchingEdges.map(elementToRemoveChange); + + onEdgesDelete?.(matchingEdges); + triggerEdgeChanges(edgeChanges); + } + + if (hasMatchingNodes) { + const nodeChanges: NodeRemoveChange[] = matchingNodes.map(elementToRemoveChange); + + onNodesDelete?.(matchingNodes); + triggerNodeChanges(nodeChanges); + } + + if (hasMatchingNodes || hasMatchingEdges) { + onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); + } + + return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; + }, + getIntersectingNodes: (nodeOrRect, partially = true, nodes) => { + const isRect = isRectObject(nodeOrRect); + const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect); + const hasNodesOption = nodes !== undefined; + + if (!nodeRect) { + return []; + } + + return (nodes || store.getState().nodes).filter((n) => { + const internalNode = store.getState().nodeLookup.get(n.id); + + if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) { + return false; + } + + const currNodeRect = nodeToRect(hasNodesOption ? n : internalNode!); + const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); + const partiallyVisible = partially && overlappingArea > 0; + + return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; + }) as NodeType[]; + }, + isNodeIntersecting: (nodeOrRect, area, partially = true) => { + const isRect = isRectObject(nodeOrRect); + const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect); + + if (!nodeRect) { + return false; + } + + const overlappingArea = getOverlappingArea(nodeRect, area); + const partiallyVisible = partially && overlappingArea > 0; + + return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; + }, + updateNode, + updateNodeData: (id, dataUpdate, options = { replace: false }) => { + updateNode( + id, + (node) => { + const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate; + return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } }; + }, + options + ); + }, + }; + }, []); return useMemo(() => { return { + ...generalHelper, ...viewportHelper, - getNodes, - getNode, - getInternalNode, - getEdges, - getEdge, - setNodes, - setEdges, - addNodes, - addEdges, - toObject, - deleteElements, - getIntersectingNodes, - isNodeIntersecting, - updateNode, - updateNodeData, + viewportInitialized, }; - }, [ - viewportHelper, - getNodes, - getNode, - getInternalNode, - getEdges, - getEdge, - setNodes, - setEdges, - addNodes, - addEdges, - toObject, - deleteElements, - getIntersectingNodes, - isNodeIntersecting, - updateNode, - updateNodeData, - ]); + }, [viewportInitialized]); } diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index eb92b310..ef4f6f38 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -7,10 +7,8 @@ import { rendererPointToPoint, } from '@xyflow/system'; -import { useStoreApi, useStore } from '../hooks/useStore'; -import type { ViewportHelperFunctions, ReactFlowState } from '../types'; - -const selector = (s: ReactFlowState) => !!s.panZoom; +import { useStoreApi } from '../hooks/useStore'; +import type { ViewportHelperFunctions } from '../types'; /** * Hook for getting viewport helper functions. @@ -20,9 +18,8 @@ const selector = (s: ReactFlowState) => !!s.panZoom; */ const useViewportHelper = (): ViewportHelperFunctions => { const store = useStoreApi(); - const panZoomInitialized = useStore(selector); - const viewportHelperFunctions = useMemo(() => { + return useMemo(() => { return { zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }), zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }), @@ -117,11 +114,8 @@ const useViewportHelper = (): ViewportHelperFunctions => { y: rendererPosition.y + domY, }; }, - viewportInitialized: panZoomInitialized, }; - }, [panZoomInitialized]); - - return viewportHelperFunctions; + }, []); }; export default useViewportHelper; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1c5794f4..f8ba9ea6 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -38,6 +38,7 @@ export * from './types'; // system types export { + type Align, type SmoothStepPathOptions, type BezierPathOptions, ConnectionLineType, @@ -97,6 +98,7 @@ export { type EdgeRemoveChange, type EdgeAddChange, type EdgeReplaceChange, + type KeyCode, } from '@xyflow/system'; // system utils diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 5d7f521b..29f99999 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -74,7 +74,7 @@ const createStore = ({ // new dimensions and update the nodes. updateNodeInternals: (updates) => { const { - onNodesChange, + triggerNodeChanges, fitView, nodeLookup, parentLookup, @@ -120,7 +120,7 @@ const createStore = ({ if (debug) { console.log('React Flow: trigger node changes', changes); } - onNodesChange?.(changes); + triggerNodeChanges?.(changes); } }, updateNodePositions: (nodeDragItems, dragging = false) => { diff --git a/packages/react/src/styles/base.css b/packages/react/src/styles/base.css index d6be5b10..25f3719a 100644 --- a/packages/react/src/styles/base.css +++ b/packages/react/src/styles/base.css @@ -1,3 +1,4 @@ /* this will be exported as base.css and can be used for a basic styling */ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/base.css'; +@import '../../../system/src/styles/node-resizer.css'; diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 8b55920f..2f518dca 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -63,8 +63,8 @@ export type OnSelectionChangeParams = { export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; -export type FitViewParams = FitViewParamsBase; -export type FitViewOptions = FitViewOptionsBase; +export type FitViewParams = FitViewParamsBase; +export type FitViewOptions = FitViewOptionsBase; export type FitView = (fitViewOptions?: FitViewOptions) => boolean; export type OnInit = ( reactFlowInstance: ReactFlowInstance @@ -156,7 +156,6 @@ export type ViewportHelperFunctions = { * const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y }) */ flowToScreenPosition: (flowPosition: XYPosition) => XYPosition; - viewportInitialized: boolean; }; export type OnBeforeDelete = OnBeforeDeleteBase< diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 67a6d928..8ce74d1c 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -13,115 +13,70 @@ export type DeleteElementsOptions = { edges?: (Edge | { id: Edge['id'] })[]; }; -export namespace Instance { - export type GetNodes = () => NodeType[]; - export type SetNodes = ( - payload: NodeType[] | ((nodes: NodeType[]) => NodeType[]) - ) => void; - export type AddNodes = (payload: NodeType[] | NodeType) => void; - export type GetNode = (id: string) => NodeType | undefined; - export type GetInternalNode = (id: string) => InternalNode | undefined; - export type GetEdges = () => EdgeType[]; - export type SetEdges = ( - payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[]) - ) => void; - export type GetEdge = (id: string) => EdgeType | undefined; - export type AddEdges = (payload: EdgeType[] | EdgeType) => void; - export type ToObject = () => ReactFlowJsonObject< - NodeType, - EdgeType - >; - export type DeleteElements = (params: DeleteElementsOptions) => Promise<{ - deletedNodes: Node[]; - deletedEdges: Edge[]; - }>; - export type GetIntersectingNodes = ( - node: NodeType | { id: Node['id'] } | Rect, - partially?: boolean, - nodes?: NodeType[] - ) => NodeType[]; - export type IsNodeIntersecting = ( - node: NodeType | { id: Node['id'] } | Rect, - area: Rect, - partially?: boolean - ) => boolean; - - export type UpdateNode = ( - id: string, - nodeUpdate: Partial | ((node: NodeType) => Partial), - options?: { replace: boolean } - ) => void; - export type UpdateNodeData = ( - id: string, - dataUpdate: object | ((node: NodeType) => object), - options?: { replace: boolean } - ) => void; -} - -export type ReactFlowInstance = { +export type GeneralHelpers = { /** * Returns nodes. * * @returns nodes array */ - getNodes: Instance.GetNodes; + getNodes: () => NodeType[]; /** * Sets nodes. * * @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes */ - setNodes: Instance.SetNodes; + setNodes: (payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])) => void; /** * Adds nodes. * * @param payload - the nodes to add */ - addNodes: Instance.AddNodes; + addNodes: (payload: NodeType[] | NodeType) => void; /** * Returns a node by id. * * @param id - the node id * @returns the node or undefined if no node was found */ - getNode: Instance.GetNode; + getNode: (id: string) => NodeType | undefined; /** * Returns an internal node by id. * * @param id - the node id * @returns the internal node or undefined if no node was found */ - getInternalNode: Instance.GetInternalNode; + getInternalNode: (id: string) => InternalNode | undefined; /** * Returns edges. * * @returns edges array */ - getEdges: Instance.GetEdges; + getEdges: () => EdgeType[]; /** * Sets edges. * * @param payload - the edges to set or a function that receives the current edges and returns the new edges */ - setEdges: Instance.SetEdges; + setEdges: (payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])) => void; /** * Adds edges. * * @param payload - the edges to add */ - addEdges: Instance.AddEdges; + addEdges: (payload: EdgeType[] | EdgeType) => void; /** * Returns an edge by id. * * @param id - the edge id * @returns the edge or undefined if no edge was found */ - getEdge: Instance.GetEdge; + getEdge: (id: string) => EdgeType | undefined; /** * Returns the nodes, edges and the viewport as a JSON object. * * @returns the nodes, edges and the viewport as a JSON object */ - toObject: Instance.ToObject; + toObject: () => ReactFlowJsonObject; /** * Deletes nodes and edges. * @@ -130,7 +85,10 @@ export type ReactFlowInstance Promise<{ + deletedNodes: Node[]; + deletedEdges: Edge[]; + }>; /** * Returns all nodes that intersect with the given node or rect. * @@ -140,7 +98,11 @@ export type ReactFlowInstance; + getIntersectingNodes: ( + node: NodeType | { id: Node['id'] } | Rect, + partially?: boolean, + nodes?: NodeType[] + ) => NodeType[]; /** * Checks if the given node or rect intersects with the passed rect. * @@ -150,7 +112,7 @@ export type ReactFlowInstance; + isNodeIntersecting: (node: NodeType | { id: Node['id'] } | Rect, area: Rect, partially?: boolean) => boolean; /** * Updates a node. * @@ -161,7 +123,11 @@ export type ReactFlowInstance ({ position: { x: node.position.x + 10, y: node.position.y } })); */ - updateNode: Instance.UpdateNode; + updateNode: ( + id: string, + nodeUpdate: Partial | ((node: NodeType) => Partial), + options?: { replace: boolean } + ) => void; /** * Updates the data attribute of a node. * @@ -172,6 +138,17 @@ export type ReactFlowInstance; - viewportInitialized: boolean; -} & Omit; + updateNodeData: ( + id: string, + dataUpdate: object | ((node: NodeType) => object), + options?: { replace: boolean } + ) => void; +}; + +export type ReactFlowInstance = GeneralHelpers< + NodeType, + EdgeType +> & + Omit & { + viewportInitialized: boolean; + }; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index cdc83088..df215340 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -6,6 +6,8 @@ import { NodeChange, NodeSelectionChange, EdgeSelectionChange, + NodeRemoveChange, + EdgeRemoveChange, } from '@xyflow/system'; import type { Node, Edge, InternalNode } from '../types'; @@ -258,3 +260,10 @@ export function getElementsDiffChanges({ return changes; } + +export function elementToRemoveChange(item: T): NodeRemoveChange | EdgeRemoveChange { + return { + id: item.id, + type: 'remove', + }; +} diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d87b7e83..430f2d82 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,27 @@ # @xyflow/svelte +## 0.1.0 + +This is a bigger update for Svelte Flow to keep up with the latest changes we made for React Flow and the Svelte5 rewrite. The biggest change is the separation of user nodes (type `Node`) and internal nodes (type `InternalNode`), which includes a renaming of the `node.computed` attribute to `node.measured`. In the previous versions, we stored internals in `node[internalsSymbol]`. This doesn't exist anymore, but we only add it to our internal nodes in `node.internals.`. + +## ⚠️ Breaking + +- rename `node.computed` to `node.measured` - this attribute only includes `width` and `height` and no `positionAbsolute` anymore. For this we added the helpers `getInternalNode` and `useInternalNode` +- rename `node.parentNode` to `node.parentId` + +### More updates: + +- add `isValidConnection` for `` component +- add `fitViewOptions` for `` component +- add `getInternalNode` to `useSvelteFlow` +- add `useInternalNode` hook +- don't reset nodes and edges when svelte flow unmounts - thanks @darabos +- fix node event types - thanks @RedPhoenixQ +- make handleId and isTarget reactive - thanks @darabos +- fix MiniMap interaction for touch devices +- fix pane: pinch zoom on windows +- fix nodes: return user node in node event handlers + ## 0.0.41 - fix: re-observe nodes when not initialized diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0277272b..4a8e1bb5 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.0.41", + "version": "0.1.0", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "keywords": [ "svelte", diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index f2166cee..54fdf91f 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -53,8 +53,8 @@ $: edgeType = type || 'default'; $: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal; - $: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined; - $: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined; + $: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined; + $: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined; $: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined'); const handleEdgeSelect = useHandleEdgeSelect(); diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index f93688fc..a7b5493c 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -22,6 +22,7 @@ export let position: $$Props['position'] = Position.Top; export let style: $$Props['style'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined; + export let isValidConnection: $$Props['isValidConnection'] = undefined; export let onconnect: $$Props['onconnect'] = undefined; export let ondisconnect: $$Props['ondisconnect'] = undefined; // @todo implement connectablestart, connectableend @@ -31,12 +32,12 @@ let className: $$Props['class'] = undefined; export { className as class }; - const isTarget = type === 'target'; + $: isTarget = type === 'target'; const nodeId = getContext('svelteflow__node_id'); const connectable = getContext>('svelteflow__node_connectable'); $: isConnectable = isConnectable !== undefined ? isConnectable : $connectable; - const handleId = id || null; + $: handleId = id || null; const store = useStore(); const { @@ -45,7 +46,7 @@ nodeLookup, connectionRadius, viewport, - isValidConnection, + isValidConnection: isValidConnectionStore, lib, addEdge, onedgecreate, @@ -77,7 +78,7 @@ lib: $lib, autoPanOnConnect: $autoPanOnConnect, flowId: $flowId, - isValidConnection: $isValidConnection, + isValidConnection: isValidConnection ?? $isValidConnectionStore, updateConnection, cancelConnection, panBy, diff --git a/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte b/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte index e648604a..f894d8d9 100644 --- a/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte +++ b/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte @@ -1,35 +1,41 @@ -{#if selectedNodes && $selectionRectMode === 'nodes'} +{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
{ - dispatchNodeEvent('nodedrag', { event, targetNode: null, nodes }); + dispatch('nodedrag', { event, targetNode: null, nodes }); }, onDragStart: (event, _, __, nodes) => { - dispatchNodeEvent('nodedragstart', { event, targetNode: null, nodes }); + dispatch('nodedragstart', { event, targetNode: null, nodes }); }, onDragStop: (event, _, __, nodes) => { - dispatchNodeEvent('nodedragstop', { event, targetNode: null, nodes }); + dispatch('nodedragstop', { event, targetNode: null, nodes }); } }} on:contextmenu={onContextMenu} diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 74a30a3b..7ba52792 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -1,7 +1,7 @@ diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index 7277be5f..842eb70b 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -1,4 +1,4 @@ -import type { Node } from '$lib/types'; +import type { InternalNode, Node } from '$lib/types'; export type NodeWrapperProps = Pick< Node, @@ -32,6 +32,6 @@ export type NodeWrapperProps = Pick< resizeObserver?: ResizeObserver | null; isParent?: boolean; zIndex: number; - node: Node; + node: InternalNode; initialized: boolean; }; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index dc08aee0..9aeb308d 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -26,7 +26,7 @@ updates.set(id, { id, nodeElement: entry.target as HTMLDivElement, - forceUpdate: true + force: true }); }); diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index 05cbdb41..e616a1b4 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -35,7 +35,7 @@ } from '@xyflow/system'; import { useStore } from '$lib/store'; - import type { Node, Edge } from '$lib/types'; + import type { Node, Edge, InternalNode } from '$lib/types'; import type { PaneProps } from './types'; type $$Props = PaneProps; @@ -68,7 +68,7 @@ let container: HTMLDivElement; let containerBounds: DOMRect | null = null; - let selectedNodes: Node[] = []; + let selectedNodes: InternalNode[] = []; $: _panOnDrag = $panActivationKeyPressed || panOnDrag; $: isSelecting = diff --git a/packages/svelte/src/lib/hooks/useInternalNode.ts b/packages/svelte/src/lib/hooks/useInternalNode.ts new file mode 100644 index 00000000..16c83960 --- /dev/null +++ b/packages/svelte/src/lib/hooks/useInternalNode.ts @@ -0,0 +1,17 @@ +import { derived, type Readable } from 'svelte/store'; + +import { useStore } from '$lib/store'; +import type { InternalNode } from '$lib/types'; + +/** + * Hook to get an internal node by id. + * + * @public + * @param id - the node id + * @returns a readable with an internal node or undefined + */ +export function useInternalNode(id: string): Readable { + const { nodeLookup, nodes } = useStore(); + + return derived([nodeLookup, nodes], ([nodeLookup]) => nodeLookup.get(id)); +} diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index 6c83418f..be40af64 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -18,7 +18,7 @@ import { } from '@xyflow/system'; import { useStore } from '$lib/store'; -import type { Edge, FitViewOptions, Node } from '$lib/types'; +import type { Edge, FitViewOptions, InternalNode, Node } from '$lib/types'; import { isNode } from '$lib/utils'; /** @@ -41,6 +41,13 @@ export function useSvelteFlow(): { * @param options.duration - optional duration. If set, a transition will be applied */ zoomOut: ZoomInOut; + /** + * Returns an internal node by id. + * + * @param id - the node id + * @returns the node or undefined if no node was found + */ + getInternalNode: (id: string) => InternalNode | undefined; /** * Returns a node by id. * @@ -280,10 +287,13 @@ export function useSvelteFlow(): { } }; + const getInternalNode = (id: string) => get(nodeLookup).get(id); + return { zoomIn, zoomOut, - getNode: (id) => get(nodeLookup).get(id), + getInternalNode, + getNode: (id) => getInternalNode(id)?.internals.userNode, getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(get(nodeLookup), ids)), getEdge: (id) => get(edgeLookup).get(id), getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)), @@ -474,14 +484,17 @@ export function useSvelteFlow(): { viewport }; } - -function getElements(lookup: Map, ids: string[]) { +function getElements(lookup: Map, ids: string[]): Node[]; +function getElements(lookup: Map, ids: string[]): Edge[]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getElements(lookup: Map, ids: string[]): any[] { const result = []; for (const id of ids) { - const element = lookup.get(id); + const item = lookup.get(id); - if (element) { + if (item) { + const element = 'internals' in item ? item.internals?.userNode : item; result.push(element); } } diff --git a/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts b/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts index cbb240ab..489dec0b 100644 --- a/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts +++ b/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts @@ -23,7 +23,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals { ) as HTMLDivElement; if (nodeElement) { - updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); + updates.set(updateId, { id: updateId, nodeElement, force: true }); } }); diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 72808cda..6a646f80 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -51,6 +51,7 @@ export type { SvelteFlowStore } from '$lib/store/types'; // system types export { + type Align, type SmoothStepPathOptions, type BezierPathOptions, ConnectionLineType, diff --git a/packages/svelte/src/lib/plugins/Controls/Controls.svelte b/packages/svelte/src/lib/plugins/Controls/Controls.svelte index aedcc182..2984ad2c 100644 --- a/packages/svelte/src/lib/plugins/Controls/Controls.svelte +++ b/packages/svelte/src/lib/plugins/Controls/Controls.svelte @@ -26,6 +26,7 @@ export let ariaLabel: $$Props['aria-label'] = undefined; export let style: $$Props['style'] = undefined; export let orientation: $$Props['orientation'] = 'vertical'; + export let fitViewOptions: $$Props['fitViewOptions'] = undefined; let className: $$Props['class'] = ''; export { className as class }; @@ -63,7 +64,7 @@ }; const onFitViewHandler = () => { - fitView(); + fitView(fitViewOptions); }; const onToggleInteractivity = () => { diff --git a/packages/svelte/src/lib/plugins/Controls/types.ts b/packages/svelte/src/lib/plugins/Controls/types.ts index 1de8dee9..a4e5f9d7 100644 --- a/packages/svelte/src/lib/plugins/Controls/types.ts +++ b/packages/svelte/src/lib/plugins/Controls/types.ts @@ -1,6 +1,8 @@ import type { HTMLButtonAttributes } from 'svelte/elements'; import type { PanelPosition } from '@xyflow/system'; +import type { FitViewOptions } from '$lib/types'; + export type ControlsProps = { /** Position of the controls on the pane * @example PanelPosition.TopLeft, PanelPosition.TopRight, @@ -21,6 +23,7 @@ export type ControlsProps = { style?: string; class?: string; orientation?: 'horizontal' | 'vertical'; + fitViewOptions?: FitViewOptions; }; export type ControlButtonProps = HTMLButtonAttributes & { diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index 20b2317d..2e6a7364 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -9,10 +9,11 @@ import cc from 'classcat'; import { getBoundsOfRects, + getInternalNodesBounds, getNodeDimensions, getNodePositionWithOrigin, - getNodesBounds, - nodeHasDimensions + nodeHasDimensions, + type Rect } from '@xyflow/system'; import { useStore } from '$lib/store'; @@ -49,6 +50,7 @@ const defaultHeight = 150; const { nodes, + nodeLookup, viewport, width: containerWidth, height: containerHeight, @@ -71,7 +73,14 @@ width: $containerWidth / $viewport.zoom, height: $containerHeight / $viewport.zoom }; - $: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getNodesBounds($nodes), viewBB) : viewBB; + let boundingRect: Rect = viewBB; + + $: { + boundingRect = + $nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds($nodeLookup), viewBB) : viewBB; + $nodes; + } + $: elementWidth = width ?? defaultWidth; $: elementHeight = height ?? defaultHeight; $: scaledWidth = boundingRect.width / elementWidth; @@ -122,8 +131,9 @@ > {#if ariaLabel}{ariaLabel}{/if} - {#each $nodes as node (node.id)} - {#if nodeHasDimensions(node)} + {#each $nodes as userNode (userNode.id)} + {@const node = $nodeLookup.get(userNode.id)} + {#if node && nodeHasDimensions(node)} {@const pos = getNodePositionWithOrigin(node).positionAbsolute} {@const nodeDimesions = getNodeDimensions(node)} true); - store.nodes.set([]); - store.edges.set([]); unselectNodesAndEdges(); cancelConnection(); diff --git a/packages/svelte/src/lib/types/general.ts b/packages/svelte/src/lib/types/general.ts index fe728fae..8aaa9a0a 100644 --- a/packages/svelte/src/lib/types/general.ts +++ b/packages/svelte/src/lib/types/general.ts @@ -45,11 +45,15 @@ export type HandleComponentProps = { isConnectableStart?: boolean; /** Should you be able to connect to this handle */ isConnectableEnd?: boolean; + /** Function that is called when checking if connection is valid. + * Overrides the isValidConnection on the Flow component. + */ + isValidConnection?: IsValidConnection; onconnect?: (connections: Connection[]) => void; ondisconnect?: (connections: Connection[]) => void; }; -export type FitViewOptions = FitViewOptionsBase; +export type FitViewOptions = FitViewOptionsBase; export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; export type OnEdgeCreate = (connection: Connection) => Edge | Connection | void; diff --git a/packages/svelte/src/lib/types/nodes.ts b/packages/svelte/src/lib/types/nodes.ts index 488b306f..0e741c00 100644 --- a/packages/svelte/src/lib/types/nodes.ts +++ b/packages/svelte/src/lib/types/nodes.ts @@ -45,3 +45,14 @@ export type NodeTypes = Record< export type DefaultNodeOptions = Partial>; export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>; + +export type NodeEventMap = { + nodeclick: { node: Node; event: MouseEvent | TouchEvent }; + nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent }; + nodedrag: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; + nodedragstart: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; + nodedragstop: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; + nodemouseenter: { node: Node; event: MouseEvent | TouchEvent }; + nodemouseleave: { node: Node; event: MouseEvent | TouchEvent }; + nodemousemove: { node: Node; event: MouseEvent | TouchEvent }; +}; diff --git a/packages/svelte/src/lib/utils/index.ts b/packages/svelte/src/lib/utils/index.ts index 8c9acdf3..10f205ca 100644 --- a/packages/svelte/src/lib/utils/index.ts +++ b/packages/svelte/src/lib/utils/index.ts @@ -1,4 +1,3 @@ -import { createEventDispatcher } from 'svelte'; import { isNodeBase, isEdgeBase } from '@xyflow/system'; import type { Edge, Node } from '$lib/types'; @@ -22,15 +21,3 @@ export const isNode = (element: unknown): element */ export const isEdge = (element: unknown): element is EdgeType => isEdgeBase(element); - -export const createNodeEventDispatcher = () => - createEventDispatcher<{ - nodeclick: { node: Node; event: MouseEvent | TouchEvent }; - nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent }; - nodedrag: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; - nodedragstart: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; - nodedragstop: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent }; - nodemouseenter: { node: Node; event: MouseEvent | TouchEvent }; - nodemouseleave: { node: Node; event: MouseEvent | TouchEvent }; - nodemousemove: { node: Node; event: MouseEvent | TouchEvent }; - }>(); diff --git a/packages/svelte/src/styles/base.css b/packages/svelte/src/styles/base.css index 80bc953d..a0cf3040 100644 --- a/packages/svelte/src/styles/base.css +++ b/packages/svelte/src/styles/base.css @@ -1,6 +1,7 @@ /* this gets exported as style.css and can be used for the default theming */ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/base.css'; +@import '../../../system/src/styles/node-resizer.css'; .svelte-flow__edge-label { text-align: center; diff --git a/packages/system/package.json b/packages/system/package.json index 849a01fe..468ee9b7 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/system", - "version": "0.0.24", + "version": "0.0.25", "description": "xyflow core system that powers React Flow and Svelte Flow.", "keywords": [ "node-based UI", diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 91a08082..6ff1dd45 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -61,13 +61,13 @@ export type FitViewParamsBase = { nodeOrigin?: NodeOrigin; }; -export type FitViewOptionsBase = { +export type FitViewOptionsBase = { padding?: number; includeHiddenNodes?: boolean; minZoom?: number; maxZoom?: number; duration?: number; - nodes?: (NodeType | { id: NodeType['id'] })[]; + nodes?: (NodeType | { id: string })[]; }; export type Viewport = { @@ -104,7 +104,7 @@ export type D3ZoomInstance = ZoomBehavior; export type D3SelectionInstance = D3Selection; export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void; -export type UpdateNodeInternals = (nodeId: string) => void; +export type UpdateNodeInternals = (nodeId: string | string[]) => void; export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 53592609..6a4715b8 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -395,7 +395,7 @@ export async function getElementsToRemove { - const nodeIds = nodesToRemove.map((node) => node.id); + const nodeIds = new Set(nodesToRemove.map((node) => node.id)); const matchingNodes: NodeType[] = []; for (const node of nodes) { @@ -403,7 +403,7 @@ export async function getElementsToRemove n.id === node.parentId); if (isIncluded || parentHit) { @@ -411,13 +411,13 @@ export async function getElementsToRemove edge.id); + const edgeIds = new Set(edgesToRemove.map((edge) => edge.id)); const deletableEdges = edges.filter((edge) => edge.deletable !== false); const connectedEdges = getConnectedEdges(matchingNodes, deletableEdges); const matchingEdges: EdgeType[] = connectedEdges; for (const edge of deletableEdges) { - const isIncluded = edgeIds.includes(edge.id); + const isIncluded = edgeIds.has(edge.id); if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) { matchingEdges.push(edge); diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index bb836a32..ee8326ff 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -77,7 +77,7 @@ export function getDragItems( // returns two params: // 1. the dragged node (or the first of the list, if we are dragging a node selection) // 2. array of selected nodes (for multi selections) -export function getEventHandlerParams({ +export function getEventHandlerParams({ nodeId, dragItems, nodeLookup, @@ -85,11 +85,11 @@ export function getEventHandlerParams({ nodeId?: string; dragItems: Map; nodeLookup: Map; -}): [NodeType, NodeType[]] { - const nodesFromDragItems: NodeType[] = []; +}): [NodeBase, NodeBase[]] { + const nodesFromDragItems: NodeBase[] = []; for (const [id, dragItem] of dragItems) { - const node = nodeLookup.get(id); + const node = nodeLookup.get(id)?.internals.userNode; if (node) { nodesFromDragItems.push({ @@ -103,7 +103,8 @@ export function getEventHandlerParams({ return [nodesFromDragItems[0], nodesFromDragItems]; } - const node = nodeLookup.get(nodeId)!; + const node = nodeLookup.get(nodeId)!.internals.userNode; + return [ { ...node, diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index a7e73a7c..052ec96e 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -214,6 +214,7 @@ export function XYPanZoom({ currentTransform.x !== viewport.x || currentTransform.y !== viewport.y ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true }); } diff --git a/packages/system/src/xypanzoom/eventhandler.ts b/packages/system/src/xypanzoom/eventhandler.ts index 2f395300..2e78b90a 100644 --- a/packages/system/src/xypanzoom/eventhandler.ts +++ b/packages/system/src/xypanzoom/eventhandler.ts @@ -78,10 +78,9 @@ export function createPanOnScrollHandler({ event.stopImmediatePropagation(); const currentZoom = d3Selection.property('__zoom').k || 1; - const _isMacOs = isMacOs(); // macos sets ctrlKey=true for pinch gesture on a trackpad - if (event.ctrlKey && zoomOnPinch && _isMacOs) { + if (event.ctrlKey && zoomOnPinch) { const point = pointer(event); const pinchDelta = wheelDelta(event); const zoom = currentZoom * Math.pow(2, pinchDelta); @@ -98,7 +97,7 @@ export function createPanOnScrollHandler({ let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize; // this enables vertical scrolling with shift + scroll on windows - if (!_isMacOs && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) { + if (!isMacOs() && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) { deltaX = event.deltaY * deltaNormalize; deltaY = 0; } @@ -208,6 +207,7 @@ export function createPanZoomEndHandler({ if (event.sourceEvent?.internal) { return; } + zoomPanValues.isZoomingOrPanning = false; if ( diff --git a/packages/system/src/xypanzoom/filter.ts b/packages/system/src/xypanzoom/filter.ts index 4cde936b..105499d7 100644 --- a/packages/system/src/xypanzoom/filter.ts +++ b/packages/system/src/xypanzoom/filter.ts @@ -76,11 +76,7 @@ export function createFilter({ } // if the pane is only movable using allowed clicks - if ( - Array.isArray(panOnDrag) && - !panOnDrag.includes(event.button) && - (event.type === 'mousedown' || event.type === 'touchstart') - ) { + if (Array.isArray(panOnDrag) && !panOnDrag.includes(event.button) && event.type === 'mousedown') { return false; } diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index cad2b481..63b68396 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { type ZoomTransform, zoomIdentity } from 'd3-zoom'; import { type D3SelectionInstance, type Viewport } from '../types';