refactor(packages): change package structure
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useRef, useState, type RefObject } from 'react';
|
||||
import { XYDrag, type XYDragInstance } from '@xyflow/system';
|
||||
|
||||
import { handleNodeClick } from '../components/Nodes/utils';
|
||||
import { useStoreApi } from './useStore';
|
||||
|
||||
type UseDragParams = {
|
||||
nodeRef: RefObject<Element>;
|
||||
disabled?: boolean;
|
||||
noDragClassName?: string;
|
||||
handleSelector?: string;
|
||||
nodeId?: string;
|
||||
isSelectable?: boolean;
|
||||
};
|
||||
|
||||
function useDrag({ nodeRef, disabled = false, noDragClassName, handleSelector, nodeId, isSelectable }: UseDragParams) {
|
||||
const store = useStoreApi();
|
||||
const [dragging, setDragging] = useState<boolean>(false);
|
||||
const xyDrag = useRef<XYDragInstance>();
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeRef?.current) {
|
||||
xyDrag.current = XYDrag({
|
||||
domNode: nodeRef.current,
|
||||
getStoreItems: () => {
|
||||
const currentStore = store.getState();
|
||||
|
||||
return {
|
||||
nodes: currentStore.getNodes(),
|
||||
...store.getState(),
|
||||
};
|
||||
},
|
||||
onNodeClick: () => {
|
||||
if (nodeId) {
|
||||
handleNodeClick({
|
||||
id: nodeId,
|
||||
store,
|
||||
nodeRef: nodeRef as RefObject<HTMLDivElement>,
|
||||
});
|
||||
}
|
||||
},
|
||||
onDragStart: () => {
|
||||
setDragging(true);
|
||||
},
|
||||
onDragStop: () => {
|
||||
setDragging(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
xyDrag.current?.destroy();
|
||||
} else {
|
||||
xyDrag.current?.update({
|
||||
noDragClassName,
|
||||
handleSelector,
|
||||
domNode: nodeRef.current as Element,
|
||||
isSelectable,
|
||||
nodeId,
|
||||
});
|
||||
return () => {
|
||||
xyDrag.current?.destroy();
|
||||
};
|
||||
}
|
||||
}, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId]);
|
||||
|
||||
return dragging;
|
||||
}
|
||||
|
||||
export default useDrag;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Edge, ReactFlowState } from '../types';
|
||||
|
||||
const edgesSelector = (state: ReactFlowState) => state.edges;
|
||||
|
||||
function useEdges<EdgeData>(): Edge<EdgeData>[] {
|
||||
const edges = useStore(edgesSelector, shallow);
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
export default useEdges;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { KeyCode } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import useKeyPress from './useKeyPress';
|
||||
import useReactFlow from './useReactFlow';
|
||||
|
||||
export default ({
|
||||
deleteKeyCode,
|
||||
multiSelectionKeyCode,
|
||||
}: {
|
||||
deleteKeyCode: KeyCode | null;
|
||||
multiSelectionKeyCode: KeyCode | null;
|
||||
}): void => {
|
||||
const store = useStoreApi();
|
||||
const { deleteElements } = useReactFlow();
|
||||
|
||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteKeyPressed) {
|
||||
const { edges, getNodes } = store.getState();
|
||||
const selectedNodes = getNodes().filter((node) => node.selected);
|
||||
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
}
|
||||
}, [deleteKeyPressed]);
|
||||
|
||||
useEffect(() => {
|
||||
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
|
||||
}, [multiSelectionKeyPressed]);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { isInputDOMNode, type KeyCode } from '@xyflow/system';
|
||||
|
||||
type Keys = Array<string>;
|
||||
type PressedKeys = Set<string>;
|
||||
type KeyOrCode = 'key' | 'code';
|
||||
|
||||
export type UseKeyPressOptions = {
|
||||
target: Window | Document | HTMLElement | ShadowRoot | null;
|
||||
};
|
||||
|
||||
const doc = typeof document !== 'undefined' ? document : null;
|
||||
|
||||
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
|
||||
// a string means a single key 'a' or a combination when '+' is used 'a+d'
|
||||
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
|
||||
// user can use the single key 'a' or the combination 'd' + 's'
|
||||
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: doc }): boolean => {
|
||||
const [keyPressed, setKeyPressed] = useState(false);
|
||||
|
||||
// we need to remember if a modifier key is pressed in order to track it
|
||||
const modifierPressed = useRef(false);
|
||||
|
||||
// we need to remember the pressed keys in order to support combinations
|
||||
const pressedKeys = useRef<PressedKeys>(new Set([]));
|
||||
|
||||
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
|
||||
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
|
||||
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch
|
||||
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
|
||||
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
|
||||
// we can't find it in the list of keysToWatch.
|
||||
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
|
||||
if (keyCode !== null) {
|
||||
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
|
||||
const keys = keyCodeArr.filter((kc) => typeof kc === 'string').map((kc) => kc.split('+'));
|
||||
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
|
||||
|
||||
return [keys, keysFlat];
|
||||
}
|
||||
|
||||
return [[], []];
|
||||
}, [keyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyCode !== null) {
|
||||
const downHandler = (event: KeyboardEvent) => {
|
||||
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
|
||||
if (!modifierPressed.current && isInputDOMNode(event)) {
|
||||
return false;
|
||||
}
|
||||
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
|
||||
pressedKeys.current.add(event[keyOrCode]);
|
||||
|
||||
if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
|
||||
event.preventDefault();
|
||||
setKeyPressed(true);
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = (event: KeyboardEvent) => {
|
||||
if (!modifierPressed.current && isInputDOMNode(event)) {
|
||||
return false;
|
||||
}
|
||||
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
|
||||
|
||||
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
|
||||
setKeyPressed(false);
|
||||
pressedKeys.current.clear();
|
||||
} else {
|
||||
pressedKeys.current.delete(event[keyOrCode]);
|
||||
}
|
||||
modifierPressed.current = false;
|
||||
};
|
||||
|
||||
const resetHandler = () => {
|
||||
pressedKeys.current.clear();
|
||||
setKeyPressed(false);
|
||||
};
|
||||
|
||||
options?.target?.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
|
||||
options?.target?.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
|
||||
window.addEventListener('blur', resetHandler);
|
||||
|
||||
return () => {
|
||||
options?.target?.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
|
||||
options?.target?.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
|
||||
window.removeEventListener('blur', resetHandler);
|
||||
};
|
||||
}
|
||||
}, [keyCode, setKeyPressed]);
|
||||
|
||||
return keyPressed;
|
||||
};
|
||||
|
||||
// utils
|
||||
|
||||
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
|
||||
return (
|
||||
keyCodes
|
||||
// we only want to compare same sizes of keyCode definitions
|
||||
// and pressed keys. When the user specified 'Meta' as a key somewhere
|
||||
// this would also be truthy without this filter when user presses 'Meta' + 'r'
|
||||
.filter((keys) => isUp || keys.length === pressedKeys.size)
|
||||
// since we want to support multiple possibilities only one of the
|
||||
// combinations need to be part of the pressed keys
|
||||
.some((keys) => keys.every((k) => pressedKeys.has(k)))
|
||||
);
|
||||
}
|
||||
|
||||
function useKeyOrCode(eventCode: string, keysToWatch: KeyCode): KeyOrCode {
|
||||
return keysToWatch.includes(eventCode) ? 'code' : 'key';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
|
||||
const nodesSelector = (state: ReactFlowState) => state.getNodes();
|
||||
|
||||
function useNodes<NodeData>(): Node<NodeData>[] {
|
||||
const nodes = useStore(nodesSelector, shallow);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export default useNodes;
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState, useCallback, type SetStateAction, type Dispatch } from 'react';
|
||||
|
||||
import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
|
||||
import type { Node, NodeChange, Edge, EdgeChange } from '../types';
|
||||
|
||||
type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[];
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
|
||||
// returns a hook that can be used liked this:
|
||||
// const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
function createUseItemsState(
|
||||
applyChanges: ApplyChanges<Node, NodeChange>
|
||||
): <NodeData = any>(
|
||||
initialItems: Node<NodeData>[]
|
||||
) => [Node<NodeData>[], Dispatch<SetStateAction<Node<NodeData>[]>>, OnChange<NodeChange>];
|
||||
function createUseItemsState(
|
||||
applyChanges: ApplyChanges<Edge, EdgeChange>
|
||||
): <EdgeData = any>(
|
||||
initialItems: Edge<EdgeData>[]
|
||||
) => [Edge<EdgeData>[], Dispatch<SetStateAction<Edge<EdgeData>[]>>, OnChange<EdgeChange>];
|
||||
function createUseItemsState(
|
||||
applyChanges: ApplyChanges<any, any>
|
||||
): (initialItems: any[]) => [any[], Dispatch<SetStateAction<any[]>>, OnChange<any>] {
|
||||
return (initialItems: any[]) => {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
|
||||
const onItemsChange = useCallback((changes: any[]) => setItems((items: any) => applyChanges(changes, items)), []);
|
||||
|
||||
return [items, setItems, onItemsChange];
|
||||
};
|
||||
}
|
||||
|
||||
export const useNodesState = createUseItemsState(applyNodeChanges);
|
||||
export const useEdgesState = createUseItemsState(applyEdgeChanges);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { internalsSymbol } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
export type UseNodesInitializedOptions = {
|
||||
includeHiddenNodes?: boolean;
|
||||
};
|
||||
|
||||
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
|
||||
if (s.nodeInternals.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return s
|
||||
.getNodes()
|
||||
.filter((n) => (options.includeHiddenNodes ? true : !n.hidden))
|
||||
.every((n) => n[internalsSymbol]?.handleBounds !== undefined);
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
includeHiddenNodes: false,
|
||||
};
|
||||
|
||||
function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
|
||||
const initialized = useStore(selector(options));
|
||||
|
||||
return initialized;
|
||||
}
|
||||
|
||||
export default useNodesInitialized;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import useReactFlow from './useReactFlow';
|
||||
import type { OnInit } from '../types';
|
||||
|
||||
function useOnInitHandler(onInit: OnInit | undefined) {
|
||||
const rfInstance = useReactFlow();
|
||||
const isInitialized = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized.current && rfInstance.viewportInitialized && onInit) {
|
||||
setTimeout(() => onInit(rfInstance), 1);
|
||||
isInitialized.current = true;
|
||||
}
|
||||
}, [onInit, rfInstance.viewportInitialized]);
|
||||
}
|
||||
|
||||
export default useOnInitHandler;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useStoreApi } from './useStore';
|
||||
import type { OnSelectionChangeFunc } from '../types';
|
||||
|
||||
export type UseOnSelectionChangeOptions = {
|
||||
onChange?: OnSelectionChangeFunc;
|
||||
};
|
||||
|
||||
function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
store.setState({ onSelectionChange: onChange });
|
||||
}, [onChange]);
|
||||
}
|
||||
|
||||
export default useOnSelectionChange;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { OnViewportChange } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from './useStore';
|
||||
|
||||
export type UseOnViewportChangeOptions = {
|
||||
onStart?: OnViewportChange;
|
||||
onChange?: OnViewportChange;
|
||||
onEnd?: OnViewportChange;
|
||||
};
|
||||
|
||||
function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
store.setState({ onViewportChangeStart: onStart });
|
||||
}, [onStart]);
|
||||
|
||||
useEffect(() => {
|
||||
store.setState({ onViewportChange: onChange });
|
||||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
store.setState({ onViewportChangeEnd: onEnd });
|
||||
}, [onEnd]);
|
||||
}
|
||||
|
||||
export default useOnViewportChange;
|
||||
@@ -0,0 +1,267 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import type {
|
||||
ReactFlowInstance,
|
||||
Instance,
|
||||
NodeAddChange,
|
||||
EdgeAddChange,
|
||||
NodeResetChange,
|
||||
EdgeResetChange,
|
||||
NodeRemoveChange,
|
||||
EdgeRemoveChange,
|
||||
NodeChange,
|
||||
Node,
|
||||
Edge,
|
||||
} from '../types';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
||||
const viewportHelper = useViewportHelper();
|
||||
const store = useStoreApi();
|
||||
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeData>>(() => {
|
||||
return store
|
||||
.getState()
|
||||
.getNodes()
|
||||
.map((n) => ({ ...n }));
|
||||
}, []);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => {
|
||||
return store.getState().nodeInternals.get(id);
|
||||
}, []);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => {
|
||||
const { edges = [] } = store.getState();
|
||||
return edges.map((e) => ({ ...e }));
|
||||
}, []);
|
||||
|
||||
const getEdge = useCallback<Instance.GetEdge<EdgeData>>((id) => {
|
||||
const { edges = [] } = store.getState();
|
||||
return edges.find((e) => e.id === id);
|
||||
}, []);
|
||||
|
||||
const setNodes = useCallback<Instance.SetNodes<NodeData>>((payload) => {
|
||||
const { getNodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
|
||||
const nodes = getNodes();
|
||||
const nextNodes = typeof payload === 'function' ? payload(nodes) : payload;
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
setNodes(nextNodes);
|
||||
} else if (onNodesChange) {
|
||||
const changes =
|
||||
nextNodes.length === 0
|
||||
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
|
||||
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeData>));
|
||||
onNodesChange(changes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setEdges = useCallback<Instance.SetEdges<EdgeData>>((payload) => {
|
||||
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
|
||||
const nextEdges = typeof payload === 'function' ? payload(edges) : payload;
|
||||
|
||||
if (hasDefaultEdges) {
|
||||
setEdges(nextEdges);
|
||||
} else if (onEdgesChange) {
|
||||
const changes =
|
||||
nextEdges.length === 0
|
||||
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
|
||||
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeData>));
|
||||
onEdgesChange(changes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addNodes = useCallback<Instance.AddNodes<NodeData>>((payload) => {
|
||||
const nodes = Array.isArray(payload) ? payload : [payload];
|
||||
const { getNodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
const currentNodes = getNodes();
|
||||
const nextNodes = [...currentNodes, ...nodes];
|
||||
setNodes(nextNodes);
|
||||
} else if (onNodesChange) {
|
||||
const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeData>));
|
||||
onNodesChange(changes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addEdges = useCallback<Instance.AddEdges<EdgeData>>((payload) => {
|
||||
const nextEdges = Array.isArray(payload) ? payload : [payload];
|
||||
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
|
||||
|
||||
if (hasDefaultEdges) {
|
||||
setEdges([...edges, ...nextEdges]);
|
||||
} else if (onEdgesChange) {
|
||||
const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeData>));
|
||||
onEdgesChange(changes);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toObject = useCallback<Instance.ToObject<NodeData, EdgeData>>(() => {
|
||||
const { getNodes, edges = [], transform } = store.getState();
|
||||
const [x, y, zoom] = transform;
|
||||
return {
|
||||
nodes: getNodes().map((n) => ({ ...n })),
|
||||
edges: edges.map((e) => ({ ...e })),
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const deleteElements = useCallback<Instance.DeleteElements>(({ nodes: nodesDeleted, edges: edgesDeleted }) => {
|
||||
const {
|
||||
nodeInternals,
|
||||
getNodes,
|
||||
edges,
|
||||
hasDefaultNodes,
|
||||
hasDefaultEdges,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
} = store.getState();
|
||||
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
|
||||
nodesToRemove: nodesDeleted || [],
|
||||
edgesToRemove: edgesDeleted || [],
|
||||
nodes: getNodes(),
|
||||
edges,
|
||||
});
|
||||
|
||||
if (matchingNodes.length || matchingEdges.length) {
|
||||
if (hasDefaultEdges || hasDefaultNodes) {
|
||||
if (hasDefaultEdges) {
|
||||
store.setState({
|
||||
edges: edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
matchingNodes.forEach((node) => {
|
||||
nodeInternals.delete(node.id);
|
||||
});
|
||||
|
||||
store.setState({
|
||||
nodeInternals: new Map(nodeInternals),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingEdges.length > 0) {
|
||||
onEdgesDelete?.(matchingEdges);
|
||||
|
||||
if (onEdgesChange) {
|
||||
onEdgesChange(
|
||||
matchingEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
type: 'remove',
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingNodes.length > 0) {
|
||||
onNodesDelete?.(matchingNodes as Node[]);
|
||||
|
||||
if (onNodesChange) {
|
||||
const nodeChanges: NodeChange[] = matchingNodes.map((node) => ({ id: node.id, type: 'remove' }));
|
||||
onNodesChange(nodeChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getNodeRect = useCallback(
|
||||
(
|
||||
nodeOrRect: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect
|
||||
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const node = isRect ? null : store.getState().nodeInternals.get(nodeOrRect.id);
|
||||
|
||||
if (!isRect && !node) {
|
||||
[null, null, isRect];
|
||||
}
|
||||
|
||||
const nodeRect = isRect ? nodeOrRect : nodeToRect(node!);
|
||||
|
||||
return [nodeRect, node, isRect];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeData>>(
|
||||
(nodeOrRect, partially = true, nodes) => {
|
||||
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
|
||||
|
||||
if (!nodeRect) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (nodes || store.getState().getNodes()).filter((n) => {
|
||||
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currNodeRect = nodeToRect(n);
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeData>>(
|
||||
(nodeOrRect, area, partially = true) => {
|
||||
const [nodeRect] = getNodeRect(nodeOrRect);
|
||||
|
||||
if (!nodeRect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
toObject,
|
||||
deleteElements,
|
||||
getIntersectingNodes,
|
||||
isNodeIntersecting,
|
||||
};
|
||||
}, [
|
||||
viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
toObject,
|
||||
deleteElements,
|
||||
getIntersectingNodes,
|
||||
isNodeIntersecting,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect, type MutableRefObject } from 'react';
|
||||
import { errorMessages, getDimensions } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
|
||||
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
const updateDimensions = () => {
|
||||
if (!rendererNode.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size = getDimensions(rendererNode.current);
|
||||
|
||||
if (size.height === 0 || size.width === 0) {
|
||||
store.getState().onError?.('004', errorMessages['error004']());
|
||||
}
|
||||
|
||||
store.setState({ width: size.width || 500, height: size.height || 500 });
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
|
||||
if (rendererNode.current) {
|
||||
resizeObserver = new ResizeObserver(() => updateDimensions());
|
||||
resizeObserver.observe(rendererNode.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDimensions);
|
||||
|
||||
if (resizeObserver && rendererNode.current) {
|
||||
resizeObserver.unobserve(rendererNode.current!);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default useResizeHandler;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useStore as useZustandStore, type StoreApi } from 'zustand';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import StoreContext from '../contexts/RFStoreContext';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
const zustandErrorMessage = errorMessages['error001']();
|
||||
|
||||
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
|
||||
|
||||
function useStore<StateSlice = ExtractState>(
|
||||
selector: (state: ReactFlowState) => StateSlice,
|
||||
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
|
||||
) {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
if (store === null) {
|
||||
throw new Error(zustandErrorMessage);
|
||||
}
|
||||
|
||||
return useZustandStore(store, selector, equalityFn);
|
||||
}
|
||||
|
||||
const useStoreApi = () => {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
if (store === null) {
|
||||
throw new Error(zustandErrorMessage);
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
getState: store.getState,
|
||||
setState: store.setState,
|
||||
subscribe: store.subscribe,
|
||||
destroy: store.destroy,
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
};
|
||||
|
||||
export { useStore, useStoreApi };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
|
||||
function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const store = useStoreApi();
|
||||
|
||||
return useCallback<UpdateNodeInternals>((id: string | string[]) => {
|
||||
const { domNode, updateNodeDimensions } = store.getState();
|
||||
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = updateIds.reduce<NodeDimensionUpdate[]>((res, updateId) => {
|
||||
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
res.push({ id: updateId, nodeElement, forceUpdate: true });
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default useUpdateNodeInternals;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCallback } from 'react';
|
||||
import { calcNextPosition } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
|
||||
function useUpdateNodePositions() {
|
||||
const store = useStoreApi();
|
||||
|
||||
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
|
||||
const { nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid, onError, nodesDraggable } =
|
||||
store.getState();
|
||||
const nodes = getNodes();
|
||||
const selectedNodes = nodes.filter(
|
||||
(n) => n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'))
|
||||
);
|
||||
// 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.
|
||||
const xVelo = snapToGrid ? snapGrid[0] : 5;
|
||||
const yVelo = snapToGrid ? snapGrid[1] : 5;
|
||||
const factor = params.isShiftPressed ? 4 : 1;
|
||||
|
||||
const positionDiffX = params.x * xVelo * factor;
|
||||
const positionDiffY = params.y * yVelo * factor;
|
||||
|
||||
const nodeUpdates = selectedNodes.map((n) => {
|
||||
if (n.positionAbsolute) {
|
||||
const nextPosition = { x: n.positionAbsolute.x + positionDiffX, y: n.positionAbsolute.y + positionDiffY };
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
|
||||
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
|
||||
}
|
||||
|
||||
const { positionAbsolute, position } = calcNextPosition(n, nextPosition, nodes, nodeExtent, undefined, onError);
|
||||
|
||||
n.position = position;
|
||||
n.positionAbsolute = positionAbsolute;
|
||||
}
|
||||
|
||||
return n;
|
||||
});
|
||||
|
||||
updateNodePositions(nodeUpdates, true, false);
|
||||
}, []);
|
||||
|
||||
return updatePositions;
|
||||
}
|
||||
|
||||
export default useUpdateNodePositions;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import type { Viewport } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
const viewportSelector = (state: ReactFlowState) => ({
|
||||
x: state.transform[0],
|
||||
y: state.transform[1],
|
||||
zoom: state.transform[2],
|
||||
});
|
||||
|
||||
function useViewport(): Viewport {
|
||||
const viewport = useStore(viewportSelector, shallow);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
export default useViewport;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useMemo } from 'react';
|
||||
import { pointToRendererPoint, getTransformForBounds, fitView, type XYPosition } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi, useStore } from '../hooks/useStore';
|
||||
import type { ViewportHelperFunctions, ReactFlowState } from '../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => !!s.panZoom;
|
||||
|
||||
const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
const store = useStoreApi();
|
||||
const panZoomInitialized = useStore(selector);
|
||||
|
||||
const viewportHelperFunctions = useMemo<ViewportHelperFunctions>(() => {
|
||||
return {
|
||||
zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }),
|
||||
zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }),
|
||||
zoomTo: (zoomLevel, options) => store.getState().panZoom?.scaleTo(zoomLevel, { duration: options?.duration }),
|
||||
getZoom: () => store.getState().transform[2],
|
||||
setViewport: (viewport, options) => {
|
||||
const {
|
||||
transform: [tX, tY, tZoom],
|
||||
panZoom,
|
||||
} = store.getState();
|
||||
|
||||
panZoom?.setViewport(
|
||||
{
|
||||
x: viewport.x ?? tX,
|
||||
y: viewport.y ?? tY,
|
||||
zoom: viewport.zoom ?? tZoom,
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
);
|
||||
},
|
||||
getViewport: () => {
|
||||
const [x, y, zoom] = store.getState().transform;
|
||||
return { x, y, zoom };
|
||||
},
|
||||
fitView: (options) => {
|
||||
const { getNodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
|
||||
|
||||
return panZoom
|
||||
? fitView(
|
||||
{
|
||||
nodes: getNodes(),
|
||||
width,
|
||||
height,
|
||||
nodeOrigin,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
panZoom,
|
||||
},
|
||||
options
|
||||
)
|
||||
: false;
|
||||
},
|
||||
setCenter: (x, y, options) => {
|
||||
const { width, height, maxZoom, panZoom } = store.getState();
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
|
||||
const centerX = width / 2 - x * nextZoom;
|
||||
const centerY = height / 2 - y * nextZoom;
|
||||
|
||||
panZoom?.setViewport(
|
||||
{
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
zoom: nextZoom,
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
);
|
||||
},
|
||||
fitBounds: (bounds, options) => {
|
||||
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
|
||||
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1);
|
||||
|
||||
panZoom?.setViewport(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
);
|
||||
},
|
||||
project: (position: XYPosition) => {
|
||||
const { transform, snapToGrid, snapGrid } = store.getState();
|
||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
|
||||
},
|
||||
viewportInitialized: panZoomInitialized,
|
||||
};
|
||||
}, [panZoomInitialized]);
|
||||
|
||||
return viewportHelperFunctions;
|
||||
};
|
||||
|
||||
export default useViewportHelper;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useCallback } from 'react';
|
||||
import { internalsSymbol, isNumeric } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import { isEdgeVisible } from '../container/EdgeRenderer/utils';
|
||||
import { type ReactFlowState, type NodeInternals, type Edge } from '../types';
|
||||
|
||||
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
|
||||
|
||||
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect = false) {
|
||||
let maxLevel = -1;
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
|
||||
const hasZIndex = isNumeric(edge.zIndex);
|
||||
let z = hasZIndex ? edge.zIndex! : 0;
|
||||
|
||||
if (elevateEdgesOnSelect) {
|
||||
z = hasZIndex
|
||||
? edge.zIndex!
|
||||
: Math.max(
|
||||
nodeInternals.get(edge.source)?.[internalsSymbol]?.z || 0,
|
||||
nodeInternals.get(edge.target)?.[internalsSymbol]?.z || 0
|
||||
);
|
||||
}
|
||||
|
||||
if (tree[z]) {
|
||||
tree[z].push(edge);
|
||||
} else {
|
||||
tree[z] = [edge];
|
||||
}
|
||||
|
||||
maxLevel = z > maxLevel ? z : maxLevel;
|
||||
|
||||
return tree;
|
||||
}, {});
|
||||
|
||||
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
|
||||
const level = +key;
|
||||
|
||||
return {
|
||||
edges,
|
||||
level,
|
||||
isMaxLevel: level === maxLevel,
|
||||
};
|
||||
});
|
||||
|
||||
if (edgeTree.length === 0) {
|
||||
return defaultEdgeTree;
|
||||
}
|
||||
|
||||
return edgeTree;
|
||||
}
|
||||
|
||||
function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean) {
|
||||
const edges = useStore(
|
||||
useCallback(
|
||||
(s: ReactFlowState) => {
|
||||
if (!onlyRenderVisible) {
|
||||
return s.edges;
|
||||
}
|
||||
|
||||
return s.edges.filter((e) => {
|
||||
const sourceNode = nodeInternals.get(e.source);
|
||||
const targetNode = nodeInternals.get(e.target);
|
||||
|
||||
return (
|
||||
sourceNode?.width &&
|
||||
sourceNode?.height &&
|
||||
targetNode?.width &&
|
||||
targetNode?.height &&
|
||||
isEdgeVisible({
|
||||
sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 },
|
||||
targetPos: targetNode.positionAbsolute || { x: 0, y: 0 },
|
||||
sourceWidth: sourceNode.width,
|
||||
sourceHeight: sourceNode.height,
|
||||
targetWidth: targetNode.width,
|
||||
targetHeight: targetNode.height,
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
transform: s.transform,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[onlyRenderVisible, nodeInternals]
|
||||
)
|
||||
);
|
||||
|
||||
return groupEdgesByZLevel(edges, nodeInternals, elevateEdgesOnSelect);
|
||||
}
|
||||
|
||||
export default useVisibleEdges;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
|
||||
function useVisibleNodes(onlyRenderVisible: boolean) {
|
||||
const nodes = useStore(
|
||||
useCallback(
|
||||
(s: ReactFlowState) =>
|
||||
onlyRenderVisible
|
||||
? getNodesInside<Node>(s.getNodes(), { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
|
||||
: s.getNodes(),
|
||||
[onlyRenderVisible]
|
||||
)
|
||||
);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export default useVisibleNodes;
|
||||
Reference in New Issue
Block a user