Merge pull request #3830 from xyflow/next

React Flow 12.0.0-next.8, Svelte Flow 0.0.35
This commit is contained in:
Moritz Klack
2024-01-25 21:20:36 +01:00
committed by GitHub
69 changed files with 940 additions and 319 deletions
+2 -1
View File
@@ -10,9 +10,10 @@ import {
Edge, Edge,
useReactFlow, useReactFlow,
Panel, Panel,
OnNodeDrag,
} from '@xyflow/react'; } from '@xyflow/react';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); const onNodeDrag: OnNodeDrag = (_, node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
@@ -1,6 +1,8 @@
import React, { memo, FC, CSSProperties, useCallback, useEffect } from 'react'; import React, { memo, FC, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react'; import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
import type { ColorSelectorNode } from '.';
const targetHandleStyle: CSSProperties = { background: '#555' }; const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = { const sourceHandleStyleB: CSSProperties = {
@@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => { const ColorSelectorNode: FC<NodeProps<ColorSelectorNode['data']>> = ({ data, isConnectable }) => {
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []); const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []); const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []); const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
@@ -5,24 +5,33 @@ import {
Controls, Controls,
addEdge, addEdge,
Node, Node,
ReactFlowInstance,
Position, Position,
SnapGrid, SnapGrid,
Connection,
useNodesState,
useEdgesState, useEdgesState,
Background, Background,
Edge, Edge,
OnNodeDrag,
OnInit,
applyNodeChanges,
OnNodesChange,
OnConnect,
OnBeforeDelete,
} from '@xyflow/react'; } from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode'; import ColorSelectorNode from './ColorSelectorNode';
const onInit = (reactFlowInstance: ReactFlowInstance) => { export type ColorSelectorNode = Node<
{ color: string; onChange: (event: ChangeEvent<HTMLInputElement>) => void },
'selectorNode'
>;
export type MyNode = Node | ColorSelectorNode;
const onInit: OnInit<MyNode> = (reactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance); console.log('flow loaded:', reactFlowInstance);
}; };
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
const initBgColor = '#1A192B'; const initBgColor = '#1A192B';
@@ -34,7 +43,16 @@ const nodeTypes = {
}; };
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]); const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange = useCallback(
(changes) =>
setNodes((nds) => {
const nextNodes = applyNodeChanges(changes, nds);
return nextNodes;
}),
[setNodes]
);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor); const [bgColor, setBgColor] = useState<string>(initBgColor);
@@ -120,12 +138,13 @@ const CustomNodeFlow = () => {
]); ]);
}, []); }, []);
const onConnect = useCallback( const onConnect: OnConnect = useCallback(
(connection: Connection) => (connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges] [setEdges]
); );
const onBeforeDelete: OnBeforeDelete<MyNode> = useCallback(async (params) => true, []);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -143,16 +162,17 @@ const CustomNodeFlow = () => {
fitView fitView
minZoom={0.3} minZoom={0.3}
maxZoom={2} maxZoom={2}
onBeforeDelete={onBeforeDelete}
> >
<MiniMap <MiniMap
nodeStrokeColor={(n: Node): string => { nodeStrokeColor={(n: MyNode): string => {
if (n.type === 'input') return '#0041d0'; if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor; if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072'; if (n.type === 'output') return '#ff0072';
return '#eee'; return '#eee';
}} }}
nodeColor={(n: Node): string => { nodeColor={(n: MyNode): string => {
if (n.type === 'selectorNode') return bgColor; if (n.type === 'selectorNode') return bgColor;
return '#fff'; return '#fff';
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
custom2: CustomEdge2, custom2: CustomEdge2,
}; };
const defaultEdgeOptions = {
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'red',
width: 20,
height: 20,
},
};
const EdgesFlow = () => { const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -197,6 +206,7 @@ const EdgesFlow = () => {
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log} onDelete={console.log}
defaultEdgeOptions={defaultEdgeOptions}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
@@ -13,6 +13,7 @@ import {
MiniMap, MiniMap,
Background, Background,
Panel, Panel,
NodeOrigin,
} from '@xyflow/react'; } from '@xyflow/react';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
@@ -119,6 +120,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
extent: 'parent',
}, },
]; ];
@@ -18,7 +18,7 @@ import UppercaseNode from './UppercaseNode';
export type TextNode = Node<{ text: string }, 'text'>; export type TextNode = Node<{ text: string }, 'text'>;
export type ResultNode = Node<{}, 'result'>; export type ResultNode = Node<{}, 'result'>;
export type UppercaseNode = Node<{}, 'uppercase'>; export type UppercaseNode = Node<{}, 'uppercase'>;
export type MyNode = Node<{ text: string }, 'text'> | Node<{}, 'result'> | Node<{}, 'uppercase'>; export type MyNode = Node | TextNode | ResultNode | UppercaseNode;
const nodeTypes = { const nodeTypes = {
text: TextNode, text: TextNode,
@@ -34,7 +34,7 @@
// See of connection landed inside the flow pane // See of connection landed inside the flow pane
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-flow__pane'); const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-flow__pane');
if (targetIsPane) { if (targetIsPane && 'clientX' in event && 'clientY' in event) {
const id = getId(); const id = getId();
const position = { const position = {
x: event.clientX, x: event.clientX,
@@ -43,10 +43,8 @@
export let zIndex: $$Props['zIndex'] = undefined; export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
x: 0, export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
y: 0
};
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined;
data; data;
@@ -59,7 +57,8 @@
zIndex; zIndex;
dragging; dragging;
dragHandle; dragHandle;
positionAbsolute; positionAbsoluteX;
positionAbsoluteY;
isConnectable; isConnectable;
</script> </script>
@@ -22,6 +22,7 @@
import CustomEdge from './CustomEdge.svelte'; import CustomEdge from './CustomEdge.svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
import InitTracker from './InitTracker.svelte';
const nodeTypes: NodeTypes = { const nodeTypes: NodeTypes = {
custom: CustomNode, custom: CustomNode,
@@ -148,6 +149,7 @@
selectionMode={SelectionMode.Full} selectionMode={SelectionMode.Full}
initialViewport={{ x: 100, y: 100, zoom: 2 }} initialViewport={{ x: 100, y: 100, zoom: 2 }}
snapGrid={[25, 25]} snapGrid={[25, 25]}
oninit={() => console.log('on init')}
on:nodeclick={(event) => console.log('on node click', event)} on:nodeclick={(event) => console.log('on node click', event)}
on:nodemouseenter={(event) => console.log('on node enter', event)} on:nodemouseenter={(event) => console.log('on node enter', event)}
on:nodemouseleave={(event) => console.log('on node leave', event)} on:nodemouseleave={(event) => console.log('on node leave', event)}
@@ -207,6 +209,8 @@
}}>hide/unhide</button }}>hide/unhide</button
> >
</Panel> </Panel>
<InitTracker />
</SvelteFlow> </SvelteFlow>
<style> <style>
@@ -0,0 +1,18 @@
<script lang="ts">
import { useNodesInitialized, useInitialized } from '@xyflow/svelte';
const nodesInitialized = useNodesInitialized();
const initialized = useInitialized();
$: {
if (nodesInitialized) {
console.log('nodes initialized');
}
}
$: {
if (initialized) {
console.log('initialized');
}
}
</script>
+17 -3
View File
@@ -1,19 +1,33 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.8
### Patch changes
- selection box is not interrupted by selectionKey being let go
- fix `OnNodeDrag` type
- do not use fallback handle if a specific id is being used
- fix `defaultEdgeOptions` markers not being applied
- fix `getNodesBounds` and add second param for passing options
- fix `expandParent` for child nodes
## 12.0.0-next.7 ## 12.0.0-next.7
## Minor changes ## Minor changes
- pass Node/Edge types to changes thanks @FelipeEmos
- use position instead of positionAbsolute for `getNodesBounds`
- add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used - add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used
### Patch changes
- pass `Node`/ `Edge` types to changes thanks @FelipeEmos
- use position instead of positionAbsolute for `getNodesBounds`
- infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine - infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine
- refactor handles: prefix with flow id for handling nested flows - refactor handles: prefix with flow id for handling nested flows
- add comments for types like `ReactFlowProps` or `Node` for a better developer experience - add comments for types like `ReactFlowProps` or `Node` for a better developer experience
## 12.0.0-next.6 ## 12.0.0-next.6
### Minor changes ### Patch changes
- fix `deleteElements` - fix `deleteElements`
- refactor internal `applyChanges` - refactor internal `applyChanges`
@@ -25,7 +25,8 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, s.nodeOrigin), viewBB) : viewBB, boundingRect:
s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB,
rfId: s.rfId, rfId: s.rfId,
nodeOrigin: s.nodeOrigin, nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
@@ -73,7 +73,7 @@ export function NodeToolbar({
return null; return null;
} }
const nodeRect: Rect = getNodesBounds(nodes, nodeOrigin); const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
@@ -186,6 +186,7 @@ export function EdgeWrapper({
animated: edge.animated, animated: edge.animated,
inactive: !isSelectable && !onClick, inactive: !isSelectable && !onClick,
updating: updateHover, updating: updateHover,
selectable: isSelectable,
}, },
])} ])}
onClick={onEdgeClick} onClick={onEdgeClick}
@@ -22,7 +22,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { type ReactFlowState } from '../../types'; import { type ReactFlowState } from '../../types';
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>; export interface HandleComponentProps extends HandleProps, Omit<HTMLAttributes<HTMLDivElement>, 'id'> {}
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
connectOnClick: s.connectOnClick, connectOnClick: s.connectOnClick,
@@ -221,4 +221,7 @@ const HandleComponent = forwardRef<HTMLDivElement, HandleComponentProps>(
HandleComponent.displayName = 'Handle'; HandleComponent.displayName = 'Handle';
/**
* The Handle component is the part of a node that can be used to connect nodes.
*/
export const Handle = memo(HandleComponent); export const Handle = memo(HandleComponent);
@@ -22,7 +22,7 @@ export type NodesSelectionProps = {
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const selectedNodes = s.nodes.filter((n) => n.selected); const selectedNodes = s.nodes.filter((n) => n.selected);
const { width, height, x, y } = getNodesBounds(selectedNodes, s.nodeOrigin); const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
return { return {
width, width,
@@ -1,9 +1,8 @@
import { memo, useCallback } from 'react'; import { memo, useMemo } from 'react';
import { type MarkerProps, createMarkerIds } from '@xyflow/system'; import { type MarkerProps, createMarkerIds } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useMarkerSymbol } from './MarkerSymbols'; import { useMarkerSymbol } from './MarkerSymbols';
import type { ReactFlowState } from '../../types';
type MarkerDefinitionsProps = { type MarkerDefinitionsProps = {
defaultColor: string; defaultColor: string;
@@ -43,23 +42,23 @@ const Marker = ({
); );
}; };
const markerSelector =
({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) =>
(s: ReactFlowState) => {
const markers = createMarkerIds(s.edges, { id: rfId, defaultColor });
return markers;
};
const markersEqual = (a: MarkerProps[], b: MarkerProps[]) =>
// the id includes all marker options, so we just need to look at that part of the marker
!(a.length !== b.length || a.some((m, i) => m.id !== b[i].id));
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore // when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper // when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids // that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual); const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
const markers = useMemo(() => {
const markers = createMarkerIds(edges, {
id: rfId,
defaultColor,
defaultMarkerStart: defaultEdgeOptions?.markerStart,
defaultMarkerEnd: defaultEdgeOptions?.markerEnd,
});
return markers;
}, [edges, defaultEdgeOptions, rfId, defaultColor]);
if (!markers.length) { if (!markers.length) {
return null; return null;
@@ -28,7 +28,9 @@ export type FlowRendererProps = Omit<
children: ReactNode; children: ReactNode;
}; };
const selector = (s: ReactFlowState) => s.nodesSelectionActive; const selector = (s: ReactFlowState) => {
return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive };
};
const FlowRendererComponent = ({ const FlowRendererComponent = ({
children, children,
@@ -67,13 +69,13 @@ const FlowRendererComponent = ({
onViewportChange, onViewportChange,
isControlledViewport, isControlledViewport,
}: FlowRendererProps) => { }: FlowRendererProps) => {
const nodesSelectionActive = useStore(selector); const { nodesSelectionActive, userSelectionActive } = useStore(selector);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode);
const panActivationKeyPressed = useKeyPress(panActivationKeyCode); const panActivationKeyPressed = useKeyPress(panActivationKeyCode);
const panOnDrag = panActivationKeyPressed || _panOnDrag; const panOnDrag = panActivationKeyPressed || _panOnDrag;
const panOnScroll = panActivationKeyPressed || _panOnScroll; const panOnScroll = panActivationKeyPressed || _panOnScroll;
const isSelecting = selectionKeyPressed || (selectionOnDrag && panOnDrag !== true); const isSelecting = selectionKeyPressed || userSelectionActive || (selectionOnDrag && panOnDrag !== true);
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode }); useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
+13 -7
View File
@@ -10,18 +10,24 @@ const selector = (s: ReactFlowStore) => ({
position: s.connectionStartHandle ? s.connectionPosition : null, position: s.connectionStartHandle ? s.connectionPosition : null,
}); });
type UseConnectionResult = {
/** The start handle where the user interaction started or null */
startHandle: ReactFlowStore['connectionStartHandle'];
/** The target handle that's inside the connection radius or null */
endHandle: ReactFlowStore['connectionEndHandle'];
/** The current connection status 'valid', 'invalid' or null*/
status: ReactFlowStore['connectionStatus'];
/** The current connection position or null */
position: ReactFlowStore['connectionPosition'] | null;
};
/** /**
* Hook for accessing the ongoing connection. * Hook for accessing the ongoing connection.
* *
* @public * @public
* @returns ongoing connection: startHandle, endHandle, status, position * @returns ongoing connection
*/ */
export function useConnection(): { export function useConnection(): UseConnectionResult {
startHandle: ReactFlowStore['connectionStartHandle'];
endHandle: ReactFlowStore['connectionEndHandle'];
status: ReactFlowStore['connectionStatus'];
position: ReactFlowStore['connectionPosition'] | null;
} {
const ongoingConnection = useStore(selector, shallow); const ongoingConnection = useStore(selector, shallow);
return ongoingConnection; return ongoingConnection;
+2 -2
View File
@@ -5,7 +5,7 @@ import { handleNodeClick } from '../components/Nodes/utils';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
type UseDragParams = { type UseDragParams = {
nodeRef: RefObject<Element>; nodeRef: RefObject<HTMLDivElement>;
disabled?: boolean; disabled?: boolean;
noDragClassName?: string; noDragClassName?: string;
handleSelector?: string; handleSelector?: string;
@@ -39,7 +39,7 @@ export function useDrag({
handleNodeClick({ handleNodeClick({
id, id,
store, store,
nodeRef: nodeRef as RefObject<HTMLDivElement>, nodeRef,
}); });
}, },
onDragStart: () => { onDragStart: () => {
@@ -13,7 +13,7 @@ type useHandleConnectionsParams = {
}; };
/** /**
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections. * Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
* *
* @public * @public
* @param param.type - handle type 'source' or 'target' * @param param.type - handle type 'source' or 'target'
@@ -12,9 +12,15 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) =>
return false; return false;
} }
return s.nodes for (const node of s.nodes) {
.filter((n) => (options.includeHiddenNodes ? true : !n.hidden)) if (options.includeHiddenNodes || !node.hidden) {
.every((n) => n[internalsSymbol]?.handleBounds !== undefined); if (node[internalsSymbol]?.handleBounds === undefined) {
return false;
}
}
}
return true;
}; };
const defaultOptions = { const defaultOptions = {
@@ -11,7 +11,7 @@ export type UseOnSelectionChangeOptions = {
* Hook for registering an onSelectionChange handler. * Hook for registering an onSelectionChange handler.
* *
* @public * @public
* @params params.onChange - The handler to register * @param params.onChange - The handler to register
*/ */
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi(); const store = useStoreApi();
@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { calcNextPosition, snapPosition } from '@xyflow/system'; import { calculateNodePosition, snapPosition } from '@xyflow/system';
import { Node } from '../types'; import { Node } from '../types';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
@@ -17,7 +17,17 @@ export function useUpdateNodePositions() {
const store = useStoreApi(); const store = useStoreApi();
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
const { nodeExtent, nodes, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions } = store.getState(); const {
nodeExtent,
nodes,
snapToGrid,
snapGrid,
nodesDraggable,
onError,
updateNodePositions,
nodeLookup,
nodeOrigin,
} = store.getState();
const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable)); const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
// by default a node moves 5px on each key press, or 20px if shift is pressed // by default a node moves 5px on each key press, or 20px if shift is pressed
// if snap grid is enabled, we use that for the velocity. // if snap grid is enabled, we use that for the velocity.
@@ -31,27 +41,24 @@ export function useUpdateNodePositions() {
const nodeUpdates = selectedNodes.map((node) => { const nodeUpdates = selectedNodes.map((node) => {
if (node.computed?.positionAbsolute) { if (node.computed?.positionAbsolute) {
let nextPosition = { let nextPosition = {
x: node.computed?.positionAbsolute.x + xDiff, x: node.computed.positionAbsolute.x + xDiff,
y: node.computed?.positionAbsolute.y + yDiff, y: node.computed.positionAbsolute.y + yDiff,
}; };
if (snapToGrid) { if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid); nextPosition = snapPosition(nextPosition, snapGrid);
} }
const { positionAbsolute, position } = calcNextPosition( const { position, positionAbsolute } = calculateNodePosition({
node, nodeId: node.id,
nextPosition, nextPosition,
nodes, nodeLookup,
nodeExtent, nodeExtent,
undefined, nodeOrigin,
onError onError,
); });
node.position = position; node.position = position;
if (!node.computed) {
node.computed = {};
}
node.computed.positionAbsolute = positionAbsolute; node.computed.positionAbsolute = positionAbsolute;
} }
@@ -86,31 +86,31 @@ const useViewportHelper = (): ViewportHelperFunctions => {
panZoom?.setViewport(viewport, { duration: options?.duration }); panZoom?.setViewport(viewport, { duration: options?.duration });
}, },
screenToFlowPosition: (position: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
const { transform, snapGrid, domNode } = store.getState(); const { transform, snapGrid, domNode } = store.getState();
if (!domNode) { if (!domNode) {
return position; return clientPosition;
} }
const { x: domX, y: domY } = domNode.getBoundingClientRect(); const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = { const correctedPosition = {
x: position.x - domX, x: clientPosition.x - domX,
y: position.y - domY, y: clientPosition.y - domY,
}; };
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid);
}, },
flowToScreenPosition: (position: XYPosition) => { flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState(); const { transform, domNode } = store.getState();
if (!domNode) { if (!domNode) {
return position; return flowPosition;
} }
const { x: domX, y: domY } = domNode.getBoundingClientRect(); const { x: domX, y: domY } = domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, transform); const rendererPosition = rendererPointToPoint(flowPosition, transform);
return { return {
x: rendererPosition.x + domX, x: rendererPosition.x + domX,
-2
View File
@@ -67,7 +67,6 @@ export {
type OnError, type OnError,
type NodeProps, type NodeProps,
type NodeOrigin, type NodeOrigin,
type OnNodeDrag,
type OnSelectionDrag, type OnSelectionDrag,
Position, Position,
type XYPosition, type XYPosition,
@@ -80,7 +79,6 @@ export {
type ColorMode, type ColorMode,
type ColorModeClass, type ColorModeClass,
type HandleType, type HandleType,
type OnBeforeDelete,
type ShouldResize, type ShouldResize,
type OnResizeStart, type OnResizeStart,
type OnResize, type OnResize,
+4 -2
View File
@@ -6,6 +6,7 @@ import {
getViewportForBounds, getViewportForBounds,
Transform, Transform,
updateConnectionLookup, updateConnectionLookup,
devWarn,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, Node, ReactFlowStore } from '../types'; import type { Edge, Node, ReactFlowStore } from '../types';
@@ -37,7 +38,8 @@ const getInitialState = ({
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); // @todo users nodeOrigin should be used here
const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
transform = [x, y, zoom]; transform = [x, y, zoom];
} }
@@ -100,7 +102,7 @@ const getInitialState = ({
autoPanOnConnect: true, autoPanOnConnect: true,
autoPanOnNodeDrag: true, autoPanOnNodeDrag: true,
connectionRadius: 20, connectionRadius: 20,
onError: () => null, onError: devWarn,
isValidConnection: undefined, isValidConnection: undefined,
onSelectionChangeHandlers: [], onSelectionChangeHandlers: [],
-18
View File
@@ -1,21 +1,3 @@
/* this will be exported as base.css and can be used for a basic styling */ /* 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/init.css';
@import '../../../system/src/styles/base.css'; @import '../../../system/src/styles/base.css';
.react-flow {
--edge-label-background-color-default: #ffffff;
--edge-label-color-default: inherit;
}
.react-flow.dark {
--edge-label-background-color-default: #141414;
--edge-label-color-default: #f8f8f8;
}
.react-flow__edge-textbg {
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
}
.react-flow__edge-text {
fill: var(--edge-label-color, var(--edge-label-color-default));
}
+2 -12
View File
@@ -3,20 +3,10 @@
@import '../../../system/src/styles/style.css'; @import '../../../system/src/styles/style.css';
@import '../../../system/src/styles/node-resizer.css'; @import '../../../system/src/styles/node-resizer.css';
.react-flow {
--edge-label-background-color-default: #ffffff;
--edge-label-color-default: inherit;
}
.react-flow.dark {
--edge-label-background-color-default: #141414;
--edge-label-color-default: #f8f8f8;
}
.react-flow__edge-textbg { .react-flow__edge-textbg {
fill: var(--edge-label-background-color, var(--edge-label-background-color-default)); fill: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default));
} }
.react-flow__edge-text { .react-flow__edge-text {
fill: var(--edge-label-color, var(--edge-label-color-default)); fill: var(--xy-edge-label-color, var(--xy-edge-label-color-default));
} }
+7 -7
View File
@@ -21,7 +21,6 @@ import type {
IsValidConnection, IsValidConnection,
ColorMode, ColorMode,
SnapGrid, SnapGrid,
OnBeforeDelete,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -40,17 +39,18 @@ import type {
OnDelete, OnDelete,
OnNodesChange, OnNodesChange,
OnEdgesChange, OnEdgesChange,
NodeDragHandler,
NodeMouseHandler, NodeMouseHandler,
SelectionDragHandler, SelectionDragHandler,
EdgeMouseHandler, EdgeMouseHandler,
OnNodeDrag,
OnBeforeDelete,
} from '.'; } from '.';
/** /**
* ReactFlow component props. * ReactFlow component props.
* @public * @public
*/ */
export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & { export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> {
/** An array of nodes to render in a controlled flow. /** An array of nodes to render in a controlled flow.
* @example * @example
* const nodes = [ * const nodes = [
@@ -111,11 +111,11 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
/** This event handler is called when a user right clicks on a node */ /** This event handler is called when a user right clicks on a node */
onNodeContextMenu?: NodeMouseHandler; onNodeContextMenu?: NodeMouseHandler;
/** This event handler is called when a user starts to drag a node */ /** This event handler is called when a user starts to drag a node */
onNodeDragStart?: NodeDragHandler; onNodeDragStart?: OnNodeDrag;
/** This event handler is called when a user drags a node */ /** This event handler is called when a user drags a node */
onNodeDrag?: NodeDragHandler; onNodeDrag?: OnNodeDrag;
/** This event handler is called when a user stops dragging a node */ /** This event handler is called when a user stops dragging a node */
onNodeDragStop?: NodeDragHandler; onNodeDragStop?: OnNodeDrag;
/** This event handler is called when a user clicks on an edge */ /** This event handler is called when a user clicks on an edge */
onEdgeClick?: (event: ReactMouseEvent, edge: Edge) => void; onEdgeClick?: (event: ReactMouseEvent, edge: Edge) => void;
/** This event handler is called when a user right clicks on an edge */ /** This event handler is called when a user right clicks on an edge */
@@ -502,6 +502,6 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
* @example 'system' | 'light' | 'dark' * @example 'system' | 'light' | 'dark'
*/ */
colorMode?: ColorMode; colorMode?: ColorMode;
}; }
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+83 -3
View File
@@ -11,6 +11,7 @@ import {
FitBounds, FitBounds,
XYPosition, XYPosition,
NodeProps, NodeProps,
OnBeforeDeleteBase,
} from '@xyflow/system'; } from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.'; import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.';
@@ -19,7 +20,7 @@ import { ComponentType } from 'react';
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void; export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void; export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
export type OnNodesDelete = (nodes: Node[]) => void; export type OnNodesDelete<NodeType extends Node = Node> = (nodes: NodeType[]) => void;
export type OnEdgesDelete = (edges: Edge[]) => void; export type OnEdgesDelete = (edges: Edge[]) => void;
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
@@ -46,16 +47,95 @@ export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> =
) => void; ) => void;
export type ViewportHelperFunctions = { export type ViewportHelperFunctions = {
/**
* Zooms viewport in by 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
/**
* Zooms viewport out by 1 / 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
/**
* Sets the current zoom level.
*
* @param zoomLevel - the zoom level to set
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomTo: ZoomTo; zoomTo: ZoomTo;
/**
* Returns the current zoom level.
*
* @returns current zoom as a number
*/
getZoom: GetZoom; getZoom: GetZoom;
/**
* Sets the current viewport.
*
* @param viewport - the viewport to set
* @param options.duration - optional duration. If set, a transition will be applied
*/
setViewport: SetViewport; setViewport: SetViewport;
/**
* Returns the current viewport.
*
* @returns Viewport
*/
getViewport: GetViewport; getViewport: GetViewport;
/**
* Fits the view.
*
* @param options.padding - optional padding
* @param options.includeHiddenNodes - optional includeHiddenNodes
* @param options.minZoom - optional minZoom
* @param options.maxZoom - optional maxZoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.nodes - optional nodes to fit the view to
*/
fitView: FitView; fitView: FitView;
/**
* Sets the center of the view to the given position.
*
* @param x - x position
* @param y - y position
* @param options.zoom - optional zoom
*/
setCenter: SetCenter; setCenter: SetCenter;
/**
* Fits the view to the given bounds .
*
* @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding
*/
fitBounds: FitBounds; fitBounds: FitBounds;
screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; /**
flowToScreenPosition: (position: XYPosition) => XYPosition; * Converts a screen / client position to a flow position.
*
* @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @param options.snapToGrid - if true, the converted position will be snapped to the grid
* @returns position as { x: number, y: number }
*
* @example
* const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY })
*/
screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
/**
* Converts a flow position to a screen / client position.
*
* @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @returns position as { x: number, y: number }
*
* @example
* const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
*/
flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
viewportInitialized: boolean; viewportInitialized: boolean;
}; };
export type OnBeforeDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = OnBeforeDeleteBase<
NodeType,
EdgeType
>;
+93
View File
@@ -59,19 +59,112 @@ export namespace Instance {
} }
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/**
* Returns nodes.
*
* @returns nodes array
*/
getNodes: Instance.GetNodes<NodeType>; getNodes: Instance.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<NodeType>; setNodes: Instance.SetNodes<NodeType>;
/**
* Adds nodes.
*
* @param payload - the nodes to add
*/
addNodes: Instance.AddNodes<NodeType>; addNodes: Instance.AddNodes<NodeType>;
/**
* Returns a node by id.
*
* @param id - the node id
* @returns the node or undefined if no node was found
*/
getNode: Instance.GetNode<NodeType>; getNode: Instance.GetNode<NodeType>;
/**
* Returns edges.
*
* @returns edges array
*/
getEdges: Instance.GetEdges<EdgeType>; getEdges: Instance.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<EdgeType>; setEdges: Instance.SetEdges<EdgeType>;
/**
* Adds edges.
*
* @param payload - the edges to add
*/
addEdges: Instance.AddEdges<EdgeType>; addEdges: Instance.AddEdges<EdgeType>;
/**
* Returns an edge by id.
*
* @param id - the edge id
* @returns the edge or undefined if no edge was found
*/
getEdge: Instance.GetEdge<EdgeType>; getEdge: Instance.GetEdge<EdgeType>;
/**
* 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<NodeType, EdgeType>; toObject: Instance.ToObject<NodeType, EdgeType>;
/**
* Deletes nodes and edges.
*
* @param params.nodes - optional nodes array to delete
* @param params.edges - optional edges array to delete
*
* @returns a promise that resolves with the deleted nodes and edges
*/
deleteElements: Instance.DeleteElements; deleteElements: Instance.DeleteElements;
/**
* Returns all nodes that intersect with the given node or rect.
*
* @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
* @param nodes - optional nodes array to check for intersections
*
* @returns an array of intersecting nodes
*/
getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>; getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
/**
* Checks if the given node or rect intersects with the passed rect.
*
* @param node - the node or rect to check for intersections
* @param area - the rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react
*
* @returns true if the node or rect intersects with the given area
*/
isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>; isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
/**
* Updates a node.
*
* @param id - id of the node to update
* @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update
* @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged
*
* @example
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
*/
updateNode: Instance.UpdateNode<NodeType>; updateNode: Instance.UpdateNode<NodeType>;
/**
* Updates the data attribute of a node.
*
* @param id - id of the node to update
* @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
* @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
*
* @example
* updateNodeData('node-1', { label: 'A new label' });
*/
updateNodeData: Instance.UpdateNodeData<NodeType>; updateNodeData: Instance.UpdateNodeData<NodeType>;
viewportInitialized: boolean; viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>; } & Omit<ViewportHelperFunctions, 'initialized'>;
+8 -4
View File
@@ -3,11 +3,11 @@ import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/sy
import { NodeTypes } from './general'; import { NodeTypes } from './general';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/** /**
* The node data structure that gets used for the nodes prop. * The node data structure that gets used for the nodes prop.
* @public * @public
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase< export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData, NodeData,
NodeType NodeType
@@ -18,9 +18,13 @@ export type Node<NodeData = any, NodeType extends string | undefined = string |
focusable?: boolean; focusable?: boolean;
}; };
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void; export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void;
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void; export type OnNodeDrag<NodeType extends Node = Node> = (
event: ReactMouseEvent,
node: NodeType,
nodes: NodeType[]
) => void;
export type NodeWrapperProps = { export type NodeWrapperProps = {
id: string; id: string;
+2 -2
View File
@@ -17,7 +17,6 @@ import {
type PanBy, type PanBy,
type OnConnectStart, type OnConnectStart,
type OnConnectEnd, type OnConnectEnd,
type OnNodeDrag,
type OnSelectionDrag, type OnSelectionDrag,
type OnMoveStart, type OnMoveStart,
type OnMove, type OnMove,
@@ -27,7 +26,6 @@ import {
type EdgeLookup, type EdgeLookup,
type ConnectionLookup, type ConnectionLookup,
type NodeLookup, type NodeLookup,
OnBeforeDelete,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -43,6 +41,8 @@ import type {
OnSelectionChangeFunc, OnSelectionChangeFunc,
UnselectNodesAndEdgesParams, UnselectNodesAndEdgesParams,
OnDelete, OnDelete,
OnNodeDrag,
OnBeforeDelete,
} from '.'; } from '.';
export type ReactFlowStore = { export type ReactFlowStore = {
+2 -5
View File
@@ -6,10 +6,7 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) {
for (const [index, item] of updatedElements.entries()) { for (const [index, item] of updatedElements.entries()) {
if (item.id === updateItem.parentNode) { if (item.id === updateItem.parentNode) {
const parent = { ...item }; const parent = { ...item };
parent.computed ??= {};
if (!parent.computed) {
parent.computed = {};
}
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width; const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height; const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
@@ -106,7 +103,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElement = { ...element }; const updatedElement = { ...element };
for (const change of changes) { for (const change of changes) {
applyChange(change, updatedElement, elements); applyChange(change, updatedElement, updatedElements);
} }
updatedElements.push(updatedElement); updatedElements.push(updatedElement);
+19 -1
View File
@@ -1,11 +1,29 @@
# @xyflow/svelte # @xyflow/svelte
## 0.0.35
## Minor changes
- add `getNode`, `getNodes`, `getEdge` and `getEdges` to `useSvelteFlow`
- add `useInitialized` / `useNNodesInitialized` hooks and `oninit` handler
## Patch changes
- selection box is not interrupted by selectionKey being let go
- Edge label has a default background and is clickable
- do not use fallback handle if a specific id is being used
- use correct id for `<Handle />` data-id attribute
- fix `getNodesBounds` and add second param for passing options
## 0.0.34 ## 0.0.34
## Minor changes ## Minor changes
- add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used - add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used
- add slot to `Controls` - add slot to `Controls`
## Patch changes
- cleanup `ControlButton` types - cleanup `ControlButton` types
- infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine - infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine
- refactor handles: prefix with flow id for handling nested flows - refactor handles: prefix with flow id for handling nested flows
@@ -42,7 +60,7 @@
- add `onbeforedelete` handler to prevent/ manage deletions - add `onbeforedelete` handler to prevent/ manage deletions
- TSDocs for hooks and some types - TSDocs for hooks and some types
### Minor changes ### Patch changes
- new nodeDragThreshold default is 1 - new nodeDragThreshold default is 1
- refactor/simplify edge rendering - refactor/simplify edge rendering
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import cc from 'classcat'; import cc from 'classcat';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
import type { BaseEdgeProps } from './types'; import type { BaseEdgeProps } from './types';
import EdgeLabel from '../EdgeLabel/EdgeLabel.svelte';
type $$Props = BaseEdgeProps; type $$Props = BaseEdgeProps;
@@ -18,7 +18,6 @@
let className: $$Props['class'] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
// @todo, why is interactionWidth undefined after first re-render?
let interactionWidthValue = interactionWidth === undefined ? 20 : interactionWidth; let interactionWidthValue = interactionWidth === undefined ? 20 : interactionWidth;
</script> </script>
@@ -43,13 +42,7 @@
{/if} {/if}
{#if label} {#if label}
<EdgeLabelRenderer> <EdgeLabel x={labelX} y={labelY} style={labelStyle}>
<div {label}
class="svelte-flow__edge-label" </EdgeLabel>
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
style={labelStyle}
>
{label}
</div>
</EdgeLabelRenderer>
{/if} {/if}
@@ -0,0 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
let _onMount: (() => void) | undefined = undefined;
export { _onMount as onMount };
let _onDestroy: (() => void) | undefined = undefined;
export { _onDestroy as onDestroy };
onMount(() => {
_onMount?.();
return _onDestroy;
});
</script>
@@ -0,0 +1 @@
export { default as CallOnMount } from './CallOnMount.svelte';
@@ -0,0 +1,28 @@
<script lang="ts">
import { getContext } from 'svelte';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
import type { BaseEdgeProps } from '$lib/components/BaseEdge/types';
export let style: BaseEdgeProps['labelStyle'] = undefined;
export let x: BaseEdgeProps['labelX'] = undefined;
export let y: BaseEdgeProps['labelY'] = undefined;
const handleEdgeSelect = useHandleEdgeSelect();
const id = getContext<string>('svelteflow__edge_id');
</script>
<EdgeLabelRenderer>
<div
class="svelte-flow__edge-label"
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
style={'pointer-events: all;' + style}
on:click={() => {
if (id) handleEdgeSelect(id);
}}
>
<slot />
</div>
</EdgeLabelRenderer>
@@ -0,0 +1 @@
export { default as EdgeLabel } from './EdgeLabel.svelte';
@@ -1,14 +1,14 @@
<svelte:options immutable /> <svelte:options immutable />
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, setContext } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { createEventDispatcher } from 'svelte'; import { getMarkerId } from '@xyflow/system';
import { errorMessages, getMarkerId } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { BezierEdgeInternal } from '$lib/components/edges'; import { BezierEdgeInternal } from '$lib/components/edges';
import type { EdgeLayouted, Edge } from '$lib/types'; import type { EdgeLayouted, Edge } from '$lib/types';
import { get } from 'svelte/store'; import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
type $$Props = EdgeLayouted; type $$Props = EdgeLayouted;
@@ -22,7 +22,6 @@
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
export let selectable: $$Props['selectable'] = true;
export let hidden: $$Props['hidden'] = false; export let hidden: $$Props['hidden'] = false;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
@@ -43,16 +42,9 @@
let className: string = ''; let className: string = '';
export { className as class }; export { className as class };
const { setContext('svelteflow__edge_id', id);
edges,
edgeTypes, const { edgeLookup, edgeTypes, flowId } = useStore();
flowId,
selectionRect,
selectionRectMode,
multiselectionKeyPressed,
addSelectedEdges,
unselectNodesAndEdges
} = useStore();
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
edgeclick: { edge: Edge; event: MouseEvent | TouchEvent }; edgeclick: { edge: Edge; event: MouseEvent | TouchEvent };
edgecontextmenu: { edge: Edge; event: MouseEvent }; edgecontextmenu: { edge: Edge; event: MouseEvent };
@@ -62,30 +54,19 @@
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined; $: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined; $: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
const handleEdgeSelect = useHandleEdgeSelect();
function onClick(event: MouseEvent | TouchEvent) { function onClick(event: MouseEvent | TouchEvent) {
const edge = $edges.find((e) => e.id === id); const edge = $edgeLookup.get(id);
if (!edge) { if (edge) {
console.warn('012', errorMessages['error012'](id)); handleEdgeSelect(id);
return; dispatch('edgeclick', { event, edge });
} }
if (selectable) {
selectionRect.set(null);
selectionRectMode.set(null);
if (!edge.selected) {
addSelectedEdges([id]);
} else if (edge.selected && get(multiselectionKeyPressed)) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
}
}
dispatch('edgeclick', { event, edge });
} }
function onContextMenu(event: MouseEvent) { function onContextMenu(event: MouseEvent) {
const edge = $edges.find((e) => e.id === id); const edge = $edgeLookup.get(id);
if (edge) { if (edge) {
dispatch('edgecontextmenu', { event, edge }); dispatch('edgecontextmenu', { event, edge });
@@ -126,11 +126,15 @@
// @todo implement connectablestart, connectableend // @todo implement connectablestart, connectableend
</script> </script>
<!--
@component
The Handle component is the part of a node that can be used to connect nodes.
-->
<div <div
data-handleid={handleId} data-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
data-id="{flowId}-{nodeId}-{id || null}-{type}" data-id="{$flowId}-{nodeId}-{id || null}-{type}"
class={cc([ class={cc([
'svelte-flow__handle', 'svelte-flow__handle',
`svelte-flow__handle-${position}`, `svelte-flow__handle-${position}`,
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { EdgeWrapper } from '$lib/components/EdgeWrapper'; import { EdgeWrapper } from '$lib/components/EdgeWrapper';
import { CallOnMount } from '$lib/components/CallOnMount';
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition'; import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { DefaultEdgeOptions } from '$lib/types'; import type { DefaultEdgeOptions } from '$lib/types';
@@ -8,8 +9,8 @@
export let defaultEdgeOptions: DefaultEdgeOptions | undefined; export let defaultEdgeOptions: DefaultEdgeOptions | undefined;
const { const {
elementsSelectable,
visibleEdges, visibleEdges,
edgesInitialized,
edges: { setDefaultOptions } edges: { setDefaultOptions }
} = useStore(); } = useStore();
@@ -24,12 +25,6 @@
</svg> </svg>
{#each $visibleEdges as edge (edge.id)} {#each $visibleEdges as edge (edge.id)}
{@const edgeType = edge.type || 'default'}
{@const selectable = !!(
edge.selectable ||
($elementsSelectable && typeof edge.selectable === 'undefined')
)}
<EdgeWrapper <EdgeWrapper
id={edge.id} id={edge.id}
source={edge.source} source={edge.source}
@@ -54,11 +49,21 @@
ariaLabel={edge.ariaLabel} ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth} interactionWidth={edge.interactionWidth}
class={edge.class} class={edge.class}
type={edgeType} type={edge.type || 'default'}
zIndex={edge.zIndex} zIndex={edge.zIndex}
{selectable}
on:edgeclick on:edgeclick
on:edgecontextmenu on:edgecontextmenu
/> />
{/each} {/each}
{#if $visibleEdges.length > 0}
<CallOnMount
onMount={() => {
$edgesInitialized = true;
}}
onDestroy={() => {
$edgesInitialized = false;
}}
/>
{/if}
</div> </div>
@@ -73,7 +73,8 @@
let selectedNodes: Node[] = []; let selectedNodes: Node[] = [];
$: _panOnDrag = $panActivationKeyPressed || panOnDrag; $: _panOnDrag = $panActivationKeyPressed || panOnDrag;
$: isSelecting = $selectionKeyPressed || (selectionOnDrag && _panOnDrag !== true); $: isSelecting =
$selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true);
$: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user'); $: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent | TouchEvent) { function onClick(event: MouseEvent | TouchEvent) {
@@ -77,6 +77,7 @@
export let onconnectstart: $$Props['onconnectstart'] = undefined; export let onconnectstart: $$Props['onconnectstart'] = undefined;
export let onconnectend: $$Props['onconnectend'] = undefined; export let onconnectend: $$Props['onconnectend'] = undefined;
export let onbeforedelete: $$Props['onbeforedelete'] = undefined; export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let oninit: $$Props['oninit'] = undefined;
export let defaultMarkerColor = '#b1b1b7'; export let defaultMarkerColor = '#b1b1b7';
@@ -130,6 +131,16 @@
} }
} }
// Call oninit once when flow is intialized
const { initialized } = store;
let onInitCalled = false;
$: {
if (!onInitCalled && $initialized) {
oninit?.();
onInitCalled = true;
}
}
// this updates the store for simple changes // this updates the store for simple changes
// where the prop names equals the store name // where the prop names equals the store name
$: { $: {
@@ -333,4 +333,6 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
onconnectstart?: OnConnectStart; onconnectstart?: OnConnectStart;
/** When a user stops dragging a connection line, this event gets fired. */ /** When a user stops dragging a connection line, this event gets fired. */
onconnectend?: OnConnectEnd; onconnectend?: OnConnectEnd;
/** This handler gets called when the flow is finished initializing */
oninit?: () => void;
}; };
@@ -4,6 +4,7 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import zoom from '$lib/actions/zoom'; import zoom from '$lib/actions/zoom';
import type { ZoomProps } from './types'; import type { ZoomProps } from './types';
import { onMount } from 'svelte';
type $$Props = ZoomProps; type $$Props = ZoomProps;
@@ -29,12 +30,17 @@
translateExtent, translateExtent,
lib, lib,
panActivationKeyPressed, panActivationKeyPressed,
zoomActivationKeyPressed zoomActivationKeyPressed,
viewportInitialized
} = useStore(); } = useStore();
$: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 }; $: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 };
$: _panOnDrag = $panActivationKeyPressed || panOnDrag; $: _panOnDrag = $panActivationKeyPressed || panOnDrag;
$: _panOnScroll = $panActivationKeyPressed || panOnScroll; $: _panOnScroll = $panActivationKeyPressed || panOnScroll;
onMount(() => {
$viewportInitialized = true;
});
</script> </script>
<div <div
@@ -0,0 +1,39 @@
import { get } from 'svelte/store';
import { errorMessages } from '@xyflow/system';
import { useStore } from '$lib/store';
export function useHandleEdgeSelect() {
const {
edgeLookup,
selectionRect,
selectionRectMode,
multiselectionKeyPressed,
addSelectedEdges,
unselectNodesAndEdges,
elementsSelectable
} = useStore();
return (id: string) => {
const edge = get(edgeLookup).get(id);
if (!edge) {
console.warn('012', errorMessages['error012'](id));
return;
}
const selectable =
edge.selectable || (get(elementsSelectable) && typeof edge.selectable === 'undefined');
if (selectable) {
selectionRect.set(null);
selectionRectMode.set(null);
if (!edge.selected) {
addSelectedEdges([id]);
} else if (edge.selected && get(multiselectionKeyPressed)) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
}
}
};
}
@@ -0,0 +1,24 @@
import { useStore } from '$lib/store';
import type { Readable } from 'svelte/store';
/**
* Hook for seeing if nodes are initialized
* @returns - nodesInitialized Writable
*/
export function useNodesInitialized() {
const { nodesInitialized } = useStore();
return {
subscribe: nodesInitialized.subscribe
} as Readable<boolean>;
}
/**
* Hook for seeing if the flow is initialized
* @returns - initialized Writable
*/
export function useInitialized() {
const { initialized } = useStore();
return {
subscribe: initialized.subscribe
} as Readable<boolean>;
}
+184 -3
View File
@@ -24,28 +24,136 @@ import { isNode } from '$lib/utils';
* Hook for accessing the ReactFlow instance. * Hook for accessing the ReactFlow instance.
* *
* @public * @public
*
* @returns helper functions * @returns helper functions
*/ */
export function useSvelteFlow(): { export function useSvelteFlow(): {
/**
* Zooms viewport in by 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
/**
* Zooms viewport out by 1 / 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
/**
* Returns a node by id.
*
* @param id - the node id
* @returns the node or undefined if no node was found
*/
getNode: (id: string) => Node | undefined;
/**
* Returns nodes.
*
* @returns nodes array
*/
getNodes: (ids?: string[]) => Node[];
/**
* Returns an edge by id.
*
* @param id - the edge id
* @returns the edge or undefined if no edge was found
*/
getEdge: (id: string) => Edge | undefined;
/**
* Returns edges.
*
* @returns edges array
*/
getEdges: (ids?: string[]) => Edge[];
/**
* Sets the current zoom level.
*
* @param zoomLevel - the zoom level to set
* @param options.duration - optional duration. If set, a transition will be applied
*/
setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void;
/**
* Returns the current zoom level.
*
* @returns current zoom as a number
*/
getZoom: () => number; getZoom: () => number;
/**
* Sets the center of the view to the given position.
*
* @param x - x position
* @param y - y position
* @param options.zoom - optional zoom
*/
setCenter: (x: number, y: number, options?: SetCenterOptions) => void; setCenter: (x: number, y: number, options?: SetCenterOptions) => void;
/**
* Sets the current viewport.
*
* @param viewport - the viewport to set
* @param options.duration - optional duration. If set, a transition will be applied
*/
setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void;
/**
* Returns the current viewport.
*
* @returns Viewport
*/
getViewport: () => Viewport; getViewport: () => Viewport;
/**
* Fits the view.
*
* @param options.padding - optional padding
* @param options.includeHiddenNodes - optional includeHiddenNodes
* @param options.minZoom - optional minZoom
* @param options.maxZoom - optional maxZoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.nodes - optional nodes to fit the view to
*/
fitView: (options?: FitViewOptions) => void; fitView: (options?: FitViewOptions) => void;
/**
* Returns all nodes that intersect with the given node or rect.
*
* @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
* @param nodes - optional nodes array to check for intersections
*
* @returns an array of intersecting nodes
*/
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: Node | { id: Node['id'] } | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
partially?: boolean, partially?: boolean,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => Node[]; ) => Node[];
/**
* Checks if the given node or rect intersects with the passed rect.
*
* @param node - the node or rect to check for intersections
* @param area - the rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react
*
* @returns true if the node or rect intersects with the given area
*/
isNodeIntersecting: ( isNodeIntersecting: (
nodeOrRect: Node | { id: Node['id'] } | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => boolean; ) => boolean;
/**
* Fits the view to the given bounds .
*
* @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding
*/
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
/**
* Deletes nodes and edges.
*
* @param params.nodes - optional nodes array to delete
* @param params.edges - optional edges array to delete
*
* @returns a promise that resolves with the deleted nodes and edges
*/
deleteElements: ({ deleteElements: ({
nodes, nodes,
edges edges
@@ -53,19 +161,66 @@ export function useSvelteFlow(): {
nodes?: (Node | { id: Node['id'] })[]; nodes?: (Node | { id: Node['id'] })[];
edges?: (Edge | { id: Edge['id'] })[]; edges?: (Edge | { id: Edge['id'] })[];
}) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>; }) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; /**
flowToScreenPosition: (position: XYPosition) => XYPosition; * Converts a screen / client position to a flow position.
*
* @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @param options.snapToGrid - if true, the converted position will be snapped to the grid
* @returns position as { x: number, y: number }
*
* @example
* const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY })
*/
screenToFlowPosition: (
clientPosition: XYPosition,
options?: { snapToGrid: boolean }
) => XYPosition;
/**
* Converts a flow position to a screen / client position.
*
* @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @returns position as { x: number, y: number }
*
* @example
* const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
*/
flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
viewport: Writable<Viewport>; viewport: Writable<Viewport>;
/**
* Updates a node.
*
* @param id - id of the node to update
* @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update
* @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged
*
* @example
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
*/
updateNode: ( updateNode: (
id: string, id: string,
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>), nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
/**
* Updates the data attribute of a node.
*
* @param id - id of the node to update
* @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
* @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
*
* @example
* updateNodeData('node-1', { label: 'A new label' });
*/
updateNodeData: ( updateNodeData: (
id: string, id: string,
dataUpdate: object | ((node: Node) => object), dataUpdate: object | ((node: Node) => object),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
/**
* Returns the nodes, edges and the viewport as a JSON object.
*
* @returns the nodes, edges and the viewport as a JSON object
*/
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
} { } {
const { const {
@@ -82,7 +237,9 @@ export function useSvelteFlow(): {
panZoom, panZoom,
nodes, nodes,
edges, edges,
domNode domNode,
nodeLookup,
edgeLookup
} = useStore(); } = useStore();
const getNodeRect = ( const getNodeRect = (
@@ -121,6 +278,10 @@ export function useSvelteFlow(): {
return { return {
zoomIn, zoomIn,
zoomOut, zoomOut,
getNode: (id) => get(nodeLookup).get(id),
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)),
setZoom: (zoomLevel, options) => { setZoom: (zoomLevel, options) => {
get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration }); get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration });
}, },
@@ -253,6 +414,11 @@ export function useSvelteFlow(): {
_snapGrid || [1, 1] _snapGrid || [1, 1]
); );
}, },
/**
*
* @param position
* @returns
*/
flowToScreenPosition: (position: XYPosition) => { flowToScreenPosition: (position: XYPosition) => {
const _domNode = get(domNode); const _domNode = get(domNode);
@@ -269,6 +435,7 @@ export function useSvelteFlow(): {
y: rendererPosition.y + domY y: rendererPosition.y + domY
}; };
}, },
toObject: () => { toObject: () => {
return { return {
nodes: get(nodes).map((node) => ({ nodes: get(nodes).map((node) => ({
@@ -295,3 +462,17 @@ export function useSvelteFlow(): {
viewport viewport
}; };
} }
function getElements<EdgeOrNode>(lookup: Map<string, EdgeOrNode>, ids: string[]) {
const result = [];
for (const id of ids) {
const element = lookup.get(id);
if (element) {
result.push(element);
}
}
return result;
}
+1 -1
View File
@@ -31,6 +31,7 @@ export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges'; export * from '$lib/hooks/useNodesEdges';
export * from '$lib/hooks/useHandleConnections'; export * from '$lib/hooks/useHandleConnections';
export * from '$lib/hooks/useNodesData'; export * from '$lib/hooks/useNodesData';
export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized';
// types // types
export type { export type {
@@ -79,7 +80,6 @@ export {
type OnError, type OnError,
type NodeProps, type NodeProps,
type NodeOrigin, type NodeOrigin,
type OnNodeDrag,
type OnSelectionDrag, type OnSelectionDrag,
Position, Position,
type XYPosition, type XYPosition,
@@ -58,7 +58,7 @@
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0 height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
}; };
} else if (toolbarNodes.length > 1) { } else if (toolbarNodes.length > 1) {
nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin); nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin });
} }
if (nodeRect) { if (nodeRect) {
+28 -2
View File
@@ -15,7 +15,6 @@ import {
type XYPosition, type XYPosition,
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
type NodeBase,
type NodeDragItem, type NodeDragItem,
errorMessages errorMessages
} from '@xyflow/system'; } from '@xyflow/system';
@@ -67,7 +66,7 @@ export function createStore({
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
store.nodes.update((nds) => { store.nodes.update((nds) => {
return nds.map((node) => { return nds.map((node) => {
const nodeDragItem = (nodeDragItems as Array<NodeBase | NodeDragItem>).find( const nodeDragItem = (nodeDragItems as Array<Node | NodeDragItem>).find(
(ndi) => ndi.id === node.id (ndi) => ndi.id === node.id
); );
@@ -112,6 +111,10 @@ export function createStore({
} }
store.nodes.set(nextNodes); store.nodes.set(nextNodes);
if (!get(store.nodesInitialized)) {
store.nodesInitialized.set(true);
}
} }
function fitView(nodes: Node[], options?: FitViewOptions) { function fitView(nodes: Node[], options?: FitViewOptions) {
@@ -354,6 +357,29 @@ export function createStore({
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id }) ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
), ),
initialized: (() => {
let initialized = false;
const initialNodesLength = get(store.nodes).length;
const initialEdgesLength = get(store.edges).length;
return derived(
[store.nodesInitialized, store.edgesInitialized, store.viewportInitialized],
([nodesInitialized, edgesInitialized, viewportInitialized]) => {
// If it was already initialized, return true from then on
if (initialized) return initialized;
// if it hasn't been initialised check if it's now
if (initialNodesLength === 0) {
initialized = viewportInitialized;
} else if (initialEdgesLength === 0) {
initialized = viewportInitialized && nodesInitialized;
} else {
initialized = viewportInitialized && nodesInitialized && edgesInitialized;
}
return initialized;
}
);
})(),
// actions // actions
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes), syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
@@ -23,7 +23,6 @@ import {
type OnConnectStart, type OnConnectStart,
type OnConnectEnd, type OnConnectEnd,
type NodeLookup, type NodeLookup,
type OnBeforeDelete,
type EdgeLookup type EdgeLookup
} from '@xyflow/system'; } from '@xyflow/system';
@@ -47,7 +46,8 @@ import type {
Edge, Edge,
FitViewOptions, FitViewOptions,
OnDelete, OnDelete,
OnEdgeCreate OnEdgeCreate,
OnBeforeDelete
} from '$lib/types'; } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils'; import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props'; import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
@@ -92,7 +92,8 @@ export const getInitialStore = ({
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); // @todo users nodeOrigin should be used here
const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
} }
@@ -152,6 +153,10 @@ export const getInitialStore = ({
onconnect: writable<OnConnect>(undefined), onconnect: writable<OnConnect>(undefined),
onconnectstart: writable<OnConnectStart>(undefined), onconnectstart: writable<OnConnectStart>(undefined),
onconnectend: writable<OnConnectEnd>(undefined), onconnectend: writable<OnConnectEnd>(undefined),
onbeforedelete: writable<OnBeforeDelete>(undefined) onbeforedelete: writable<OnBeforeDelete>(undefined),
nodesInitialized: writable<boolean>(false),
edgesInitialized: writable<boolean>(false),
viewportInitialized: writable<boolean>(false),
initialized: readable<boolean>(false)
}; };
}; };
+6 -1
View File
@@ -5,7 +5,8 @@ import type {
Position, Position,
XYPosition, XYPosition,
ConnectingHandle, ConnectingHandle,
Connection Connection,
OnBeforeDeleteBase
} from '@xyflow/system'; } from '@xyflow/system';
import type { Node } from './nodes'; import type { Node } from './nodes';
@@ -52,3 +53,7 @@ export type FitViewOptions = FitViewOptionsBase<Node>;
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
export type OnEdgeCreate = (connection: Connection) => Edge | Connection | void; export type OnEdgeCreate = (connection: Connection) => Edge | Connection | void;
export type OnBeforeDelete<
NodeType extends Node = Node,
EdgeType extends Edge = Edge
> = OnBeforeDeleteBase<NodeType, EdgeType>;
-9
View File
@@ -2,16 +2,7 @@
@import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/init.css';
@import '../../../system/src/styles/base.css'; @import '../../../system/src/styles/base.css';
.svelte-flow {
--edge-label-color-default: inherit;
}
.svelte-flow.dark {
--edge-label-color-default: #f8f8f8;
}
.svelte-flow__edge-label { .svelte-flow__edge-label {
text-align: center; text-align: center;
position: absolute; position: absolute;
color: var(--edge-label-color, var(--edge-label-color-default));
} }
+4 -9
View File
@@ -3,19 +3,14 @@
@import '../../../system/src/styles/style.css'; @import '../../../system/src/styles/style.css';
@import '../../../system/src/styles/node-resizer.css'; @import '../../../system/src/styles/node-resizer.css';
.svelte-flow {
--edge-label-color-default: inherit;
}
.svelte-flow.dark {
--edge-label-color-default: #f8f8f8;
}
.svelte-flow__edge-label { .svelte-flow__edge-label {
text-align: center; text-align: center;
position: absolute; position: absolute;
padding: 2px;
font-size: 10px; font-size: 10px;
color: var(--edge-label-color, var(--edge-label-color-default)); cursor: pointer;
color: var(--xy-edge-label-color, var(--xy-edge-label-color-default));
background: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default));
} }
.svelte-flow__nodes { .svelte-flow__nodes {
+2 -2
View File
@@ -139,8 +139,8 @@
} }
&.selected .xy-flow__edge-path, &.selected .xy-flow__edge-path,
&:focus .xy-flow__edge-path, &.selectable:focus .xy-flow__edge-path,
&:focus-visible .xy-flow__edge-path { &.selectable:focus-visible .xy-flow__edge-path {
stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default)); stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default));
} }
+6
View File
@@ -19,6 +19,9 @@
--xy-controls-button-color-hover-default: inherit; --xy-controls-button-color-hover-default: inherit;
--xy-controls-button-border-color-default: #eee; --xy-controls-button-border-color-default: #eee;
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
--xy-edge-label-background-color-default: #ffffff;
--xy-edge-label-color-default: inherit;
} }
.xy-flow.dark { .xy-flow.dark {
@@ -41,6 +44,9 @@
--xy-controls-button-color-hover-default: #fff; --xy-controls-button-color-hover-default: #fff;
--xy-controls-button-border-color-default: #5b5b5b; --xy-controls-button-border-color-default: #5b5b5b;
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
--xy-edge-label-background-color-default: #141414;
--xy-edge-label-color-default: #f8f8f8;
} }
.xy-flow__edge { .xy-flow__edge {
+1 -1
View File
@@ -142,7 +142,7 @@ export type ColorMode = ColorModeClass | 'system';
export type ConnectionLookup = Map<string, Map<string, Connection>>; export type ConnectionLookup = Map<string, Map<string, Connection>>;
export type OnBeforeDelete = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({ export type OnBeforeDeleteBase<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase> = ({
nodes, nodes,
edges, edges,
}: { }: {
+3 -5
View File
@@ -94,9 +94,9 @@ export type NodeProps<T = any> = {
positionAbsoluteY: number; positionAbsoluteY: number;
width?: number; width?: number;
height?: number; height?: number;
dragging: boolean; dragging: NodeBase['dragging'];
targetPosition?: Position; sourcePosition?: NodeBase['sourcePosition'];
sourcePosition?: Position; targetPosition?: NodeBase['targetPosition'];
}; };
export type NodeHandleBounds = { export type NodeHandleBounds = {
@@ -134,8 +134,6 @@ export type NodeDragItem = {
export type NodeOrigin = [number, number]; export type NodeOrigin = [number, number];
export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void;
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
+2 -7
View File
@@ -114,11 +114,6 @@ function getHandle(bounds: HandleElement[], handleId?: string | null): HandleEle
return null; return null;
} }
if (bounds.length === 1 || !handleId) { // if no handleId is given, we use the first handle, otherwise we check for the id
return bounds[0]; return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null;
} else if (handleId) {
return bounds.find((d) => d.id === handleId) || null;
}
return null;
} }
+4
View File
@@ -198,3 +198,7 @@ export const getViewportForBounds = (
}; };
export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent {
return extent !== undefined && extent !== 'parent';
}
+88 -60
View File
@@ -4,11 +4,11 @@ import {
clampPosition, clampPosition,
getBoundsOfBoxes, getBoundsOfBoxes,
getOverlappingArea, getOverlappingArea,
isNumeric,
rectToBox, rectToBox,
nodeToRect, nodeToRect,
pointToRendererPoint, pointToRendererPoint,
getViewportForBounds, getViewportForBounds,
isCoordinateExtent,
} from './general'; } from './general';
import { import {
type Transform, type Transform,
@@ -19,10 +19,10 @@ import {
type EdgeBase, type EdgeBase,
type FitViewParamsBase, type FitViewParamsBase,
type FitViewOptionsBase, type FitViewOptionsBase,
NodeDragItem,
CoordinateExtent, CoordinateExtent,
OnError, OnError,
OnBeforeDelete, OnBeforeDeleteBase,
NodeLookup,
} from '../types'; } from '../types';
import { errorMessages } from '../constants'; import { errorMessages } from '../constants';
@@ -102,11 +102,13 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (
node: NodeBase | undefined, node: NodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): XYPosition & { positionAbsolute: XYPosition } => { ): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) { if (!node) {
return { return {
x: 0, position: {
y: 0, x: 0,
y: 0,
},
positionAbsolute: { positionAbsolute: {
x: 0, x: 0,
y: 0, y: 0,
@@ -123,7 +125,7 @@ export const getNodePositionWithOrigin = (
}; };
return { return {
...position, position,
positionAbsolute: node.computed?.positionAbsolute positionAbsolute: node.computed?.positionAbsolute
? { ? {
x: node.computed.positionAbsolute.x - offsetX, x: node.computed.positionAbsolute.x - offsetX,
@@ -133,27 +135,35 @@ export const getNodePositionWithOrigin = (
}; };
}; };
export type GetNodesBoundsParams = {
nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean;
};
/** /**
* Determines a bounding box that contains all given nodes in an array * Determines a bounding box that contains all given nodes in an array
* @public * @public
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport. * @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
* @param nodes - Nodes to calculate the bounds for * @param nodes - Nodes to calculate the bounds for
* @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
* @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes * @returns Bounding box enclosing all nodes
*/ */
export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0]): Rect => { export const getNodesBounds = (
nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
): Rect => {
if (nodes.length === 0) { if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 }; return { x: 0, y: 0, width: 0, height: 0 };
} }
const box = nodes.reduce( const box = nodes.reduce(
(currBox, node) => { (currBox, node) => {
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
return getBoundsOfBoxes( return getBoundsOfBoxes(
currBox, currBox,
rectToBox({ rectToBox({
x, ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
y,
width: node.computed?.width ?? node.width ?? 0, width: node.computed?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 0, height: node.computed?.height ?? node.height ?? 0,
}) })
@@ -239,7 +249,7 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
}); });
if (filteredNodes.length > 0) { if (filteredNodes.length > 0) {
const bounds = getNodesBounds(filteredNodes, nodeOrigin); const bounds = getNodesBounds(filteredNodes, { nodeOrigin });
const viewport = getViewportForBounds( const viewport = getViewportForBounds(
bounds, bounds,
@@ -258,69 +268,87 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
return false; return false;
} }
function clampNodeExtent(node: NodeDragItem | NodeBase, extent?: CoordinateExtent | 'parent') { /**
* This function clamps the passed extend by the node's width and height.
* This is needed to prevent the node from being dragged outside of its extent.
*
* @param node
* @param extent
* @returns
*/
function clampNodeExtent<NodeType extends NodeBase>(
node: NodeType,
extent?: CoordinateExtent | 'parent'
): CoordinateExtent | 'parent' | undefined {
if (!extent || extent === 'parent') { if (!extent || extent === 'parent') {
return extent; return extent;
} }
return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]]; return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
} }
export function calcNextPosition<NodeType extends NodeBase>( /**
node: NodeDragItem | NodeType, * This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
nextPosition: XYPosition, *
nodes: NodeType[], * @internal
nodeExtent?: CoordinateExtent, * @returns position, positionAbsolute
nodeOrigin: NodeOrigin = [0, 0], */
onError?: OnError export function calculateNodePosition<NodeType extends NodeBase>({
): { position: XYPosition; positionAbsolute: XYPosition } { nodeId,
const clampedNodeExtent = clampNodeExtent(node, node.extent || nodeExtent); nextPosition,
let currentExtent = clampedNodeExtent; nodeLookup,
let parentNode: NodeType | null = null; nodeOrigin = [0, 0],
let parentPos = { x: 0, y: 0 }; nodeExtent,
onError,
if (node.parentNode) { }: {
parentNode = nodes.find((n) => n.id === node.parentNode) || null; nodeId: string;
parentPos = parentNode nextPosition: XYPosition;
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute nodeLookup: NodeLookup<NodeType>;
: parentPos; nodeOrigin?: NodeOrigin;
} nodeExtent?: CoordinateExtent;
onError?: OnError;
}): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!;
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined;
const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) { if (node.extent === 'parent' && !node.expandParent) {
const nodeWidth = node.computed?.width; if (!parentNode) {
const nodeHeight = node.computed?.height;
if (node.parentNode && nodeWidth && nodeHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
currentExtent =
parentNode && isNumeric(parentNode.computed?.width) && isNumeric(parentNode.computed?.height)
? [
[parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]],
[
parentPos.x + (parentNode.computed?.width ?? 0) - nodeWidth + nodeWidth * currNodeOrigin[0],
parentPos.y + (parentNode.computed?.height ?? 0) - nodeHeight + nodeHeight * currNodeOrigin[1],
],
]
: currentExtent;
} else {
onError?.('005', errorMessages['error005']()); onError?.('005', errorMessages['error005']());
currentExtent = clampedNodeExtent; } else {
const nodeWidth = node.computed?.width;
const nodeHeight = node.computed?.height;
const parentWidth = parentNode?.computed?.width;
const parentHeight = parentNode?.computed?.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
const extentX = parentX + nodeWidth * currNodeOrigin[0];
const extentY = parentY + nodeHeight * currNodeOrigin[1];
currentExtent = [
[extentX, extentY],
[extentX + parentWidth - nodeWidth, extentY + parentHeight - nodeHeight],
];
}
} }
} else if (node.extent && node.parentNode && node.extent !== 'parent') { } else if (parentNode && isCoordinateExtent(node.extent)) {
currentExtent = [ currentExtent = [
[node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y], [node.extent[0][0] + parentX, node.extent[0][1] + parentY],
[node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y], [node.extent[1][0] + parentX, node.extent[1][1] + parentY],
]; ];
} }
const positionAbsolute = const positionAbsolute = isCoordinateExtent(currentExtent)
currentExtent && currentExtent !== 'parent' ? clampPosition(nextPosition, currentExtent)
? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
: nextPosition;
return { return {
position: { position: {
x: positionAbsolute.x - parentPos.x, x: positionAbsolute.x - parentX,
y: positionAbsolute.y - parentPos.y, y: positionAbsolute.y - parentY,
}, },
positionAbsolute, positionAbsolute,
}; };
@@ -347,7 +375,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
edgesToRemove: Partial<EdgeType>[]; edgesToRemove: Partial<EdgeType>[];
nodes: NodeType[]; nodes: NodeType[];
edges: EdgeType[]; edges: EdgeType[];
onBeforeDelete?: OnBeforeDelete; onBeforeDelete?: OnBeforeDeleteBase<NodeType, EdgeType>;
}): Promise<{ }): Promise<{
nodes: NodeType[]; nodes: NodeType[];
edges: EdgeType[]; edges: EdgeType[];
+16 -5
View File
@@ -19,21 +19,32 @@ export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | nu
export function createMarkerIds( export function createMarkerIds(
edges: EdgeBase[], edges: EdgeBase[],
{ id, defaultColor }: { id?: string | null; defaultColor?: string } {
id,
defaultColor,
defaultMarkerStart,
defaultMarkerEnd,
}: {
id?: string | null;
defaultColor?: string;
defaultMarkerStart?: EdgeMarkerType;
defaultMarkerEnd?: EdgeMarkerType;
}
) { ) {
const ids: string[] = []; const ids = new Set<string>();
return edges return edges
.reduce<MarkerProps[]>((markers, edge) => { .reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => { [edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') { if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, id); const markerId = getMarkerId(marker, id);
if (!ids.includes(markerId)) { if (!ids.has(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker }); markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId); ids.add(markerId);
} }
} }
}); });
return markers; return markers;
}, []) }, [])
.sort((a, b) => a.id.localeCompare(b.id)); .sort((a, b) => a.id.localeCompare(b.id));
+1 -1
View File
@@ -136,7 +136,7 @@ function calculateXYZPosition<NodeType extends NodeBase>(
} }
const parentNode = nodeLookup.get(node.parentNode)!; const parentNode = nodeLookup.get(node.parentNode)!;
const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition( return calculateXYZPosition(
parentNode, parentNode,
+19 -13
View File
@@ -5,7 +5,7 @@ import {
calcAutoPan, calcAutoPan,
getEventPosition, getEventPosition,
getPointerPosition, getPointerPosition,
calcNextPosition, calculateNodePosition,
snapPosition, snapPosition,
getNodesBounds, getNodesBounds,
rectToBox, rectToBox,
@@ -23,7 +23,6 @@ import type {
SnapGrid, SnapGrid,
Transform, Transform,
PanBy, PanBy,
OnNodeDrag,
OnSelectionDrag, OnSelectionDrag,
UpdateNodePositions, UpdateNodePositions,
Box, Box,
@@ -31,7 +30,7 @@ import type {
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void; export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
type StoreItems = { type StoreItems<OnNodeDrag> = {
nodes: NodeBase[]; nodes: NodeBase[];
nodeLookup: Map<string, NodeBase>; nodeLookup: Map<string, NodeBase>;
edges: EdgeBase[]; edges: EdgeBase[];
@@ -58,9 +57,9 @@ type StoreItems = {
updateNodePositions: UpdateNodePositions; updateNodePositions: UpdateNodePositions;
}; };
export type XYDragParams = { export type XYDragParams<OnNodeDrag> = {
domNode: Element; domNode: Element;
getStoreItems: () => StoreItems; getStoreItems: () => StoreItems<OnNodeDrag>;
onDragStart?: OnDrag; onDragStart?: OnDrag;
onDrag?: OnDrag; onDrag?: OnDrag;
onDragStop?: OnDrag; onDragStop?: OnDrag;
@@ -80,14 +79,15 @@ export type DragUpdateParams = {
domNode: Element; domNode: Element;
}; };
export function XYDrag({ // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => void | undefined>({
domNode, domNode,
onNodeMouseDown, onNodeMouseDown,
getStoreItems, getStoreItems,
onDragStart, onDragStart,
onDrag, onDrag,
onDragStop, onDragStop,
}: XYDragParams): XYDragInstance { }: XYDragParams<OnNodeDrag>): XYDragInstance {
let lastPos: { x: number | null; y: number | null } = { x: null, y: null }; let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
let autoPanId = 0; let autoPanId = 0;
let dragItems: NodeDragItem[] = []; let dragItems: NodeDragItem[] = [];
@@ -103,7 +103,6 @@ export function XYDrag({
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) { function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) {
function updateNodes({ x, y }: XYPosition) { function updateNodes({ x, y }: XYPosition) {
const { const {
nodes,
nodeLookup, nodeLookup,
nodeExtent, nodeExtent,
snapGrid, snapGrid,
@@ -121,7 +120,7 @@ export function XYDrag({
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.length > 1 && nodeExtent) { if (dragItems.length > 1 && nodeExtent) {
const rect = getNodesBounds(dragItems as unknown as NodeBase[], nodeOrigin); const rect = getNodesBounds(dragItems as unknown as NodeBase[], { nodeOrigin });
nodesBox = rectToBox(rect); nodesBox = rectToBox(rect);
} }
@@ -149,13 +148,20 @@ export function XYDrag({
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
} }
const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError); const { position, positionAbsolute } = calculateNodePosition({
nodeId: n.id,
nextPosition,
nodeLookup,
nodeExtent: adjustedNodeExtent,
nodeOrigin,
onError,
});
// we want to make sure that we only fire a change event when there is a change // we want to make sure that we only fire a change event when there is a change
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y; hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
n.position = updatedPos.position; n.position = position;
n.computed.positionAbsolute = updatedPos.positionAbsolute; n.computed.positionAbsolute = positionAbsolute;
return n; return n;
}); });
+4 -3
View File
@@ -260,9 +260,10 @@ function isValidHandle(
}: IsValidParams }: IsValidParams
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector( const handleDomNode = handle
`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]` ? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
); : null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y); const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle, // we always want to prioritize the handle below the mouse cursor over the closest distance handle,