Merge pull request #3919 from xyflow/fix/remeasure-nodes

Fix/remeasure nodes
This commit is contained in:
Moritz Klack
2024-02-15 18:00:56 +01:00
committed by GitHub
9 changed files with 74 additions and 70 deletions
@@ -27,9 +27,9 @@ const buttonStyle: CSSProperties = {
zIndex: 4, zIndex: 4,
}; };
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => ( const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} /> return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
); };
const CustomMiniMapNodeFlow = () => { const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
+20 -20
View File
@@ -90,7 +90,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5' }, data: { label: 'Node 5' },
position: { x: 650, y: 250 }, position: { x: 650, y: 250 },
className: 'light', className: 'light',
style: { width: 400, height: 150 }, style: { width: 100, height: 100 },
zIndex: 1000, zIndex: 1000,
}, },
{ {
@@ -161,9 +161,12 @@ const Subflow = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
if (!n.parentNode) { if (!n.parentNode) {
n.position = { return {
x: Math.random() * 400, ...n,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
} }
@@ -178,8 +181,10 @@ const Subflow = () => {
const toggleClassnames = () => { const toggleClassnames = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light'; return {
return n; ...n,
className: n.className === 'light' ? 'dark' : 'light',
};
}); });
}); });
}; };
@@ -187,8 +192,10 @@ const Subflow = () => {
const toggleChildNodes = () => { const toggleChildNodes = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden; return {
return n; ...n,
hidden: !!n.parentNode && !n.hidden,
};
}); });
}); });
}; };
@@ -215,19 +222,12 @@ const Subflow = () => {
<Background /> <Background />
<Panel position="top-right"> <Panel position="top-right">
<button onClick={resetTransform} style={{ marginRight: 5 }}> <button onClick={resetTransform}>reset transform</button>
reset transform <button onClick={updatePos}>change pos</button>
</button> <button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={updatePos} style={{ marginRight: 5 }}> <button onClick={toggleChildNodes}>toggleChildNodes</button>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button> <button onClick={logToObject}>toObject</button>
<button onClick={() => setNodes(initialNodes)}>setNodes</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
@@ -101,12 +101,12 @@ export function EdgeWrapper({
); );
const markerStartUrl = useMemo( const markerStartUrl = useMemo(
() => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), () => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined),
[edge.markerStart, rfId] [edge.markerStart, rfId]
); );
const markerEndUrl = useMemo( const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), () => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined),
[edge.markerEnd, rfId] [edge.markerEnd, rfId]
); );
@@ -79,16 +79,33 @@ export function NodeWrapper({
const prevTargetPosition = useRef(node.targetPosition); const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType); const prevType = useRef(nodeType);
const width = node.width ?? undefined;
const height = node.height ?? undefined;
const computedWidth = node.computed?.width;
const computedHeight = node.computed?.height;
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const hasHandleBounds = !!node[internalsSymbol]?.handleBounds;
const moveSelectedNodes = useMoveSelectedNodes(); const moveSelectedNodes = useMoveSelectedNodes();
useEffect(() => {
return () => {
if (nodeRef.current) {
resizeObserver?.unobserve(nodeRef.current);
}
};
}, []);
useEffect(() => { useEffect(() => {
if (nodeRef.current && !node.hidden) { if (nodeRef.current && !node.hidden) {
const currNode = nodeRef.current; const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode); if (!initialized || !hasHandleBounds) {
resizeObserver?.unobserve(currNode);
resizeObserver?.observe(currNode);
}
} }
}, [node.hidden]); }, [node.hidden, initialized, hasHandleBounds]);
useEffect(() => { useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node // when the user programmatically changes the source or handle position, we re-initialize the node
@@ -123,11 +140,6 @@ export function NodeWrapper({
return null; return null;
} }
const width = node.width ?? undefined;
const height = node.height ?? undefined;
const computedWidth = node.computed?.width;
const computedHeight = node.computed?.height;
const positionAbsoluteOrigin = getPositionWithOrigin({ const positionAbsoluteOrigin = getPositionWithOrigin({
x: positionAbsoluteX, x: positionAbsoluteX,
y: positionAbsoluteY, y: positionAbsoluteY,
@@ -135,7 +147,6 @@ export function NodeWrapper({
height: computedHeight ?? height ?? 0, height: computedHeight ?? height ?? 0,
origin: node.origin || nodeOrigin, origin: node.origin || nodeOrigin,
}); });
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined; const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
@@ -9,7 +9,7 @@ import { infiniteExtent, type CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
import { initNodeOrigin } from '../../container/ReactFlow'; import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
// these fields exist in the global store and we need to keep them up to date // these fields exist in the global store and we need to keep them up to date
const reactFlowFieldsToTrack = [ const reactFlowFieldsToTrack = [
@@ -84,6 +84,19 @@ const selector = (s: ReactFlowState) => ({
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
}); });
const initPrevValues = {
// these are values that are also passed directly to other components
// than the StoreUpdater. We can reduce the number of setStore calls
// by setting the same values here as prev fields.
translateExtent: infiniteExtent,
nodeOrigin: defaultNodeOrigin,
minZoom: 0.5,
maxZoom: 2,
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
};
export function StoreUpdater(props: StoreUpdaterProps) { export function StoreUpdater(props: StoreUpdaterProps) {
const { const {
setNodes, setNodes,
@@ -99,23 +112,15 @@ export function StoreUpdater(props: StoreUpdaterProps) {
useEffect(() => { useEffect(() => {
setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges); setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges);
return () => { return () => {
// when we reset the store we also need to reset the previous fields
previousFields.current = initPrevValues;
reset(); reset();
}; };
}, []); }, []);
const previousFields = useRef<Partial<StoreUpdaterProps>>({ const previousFields = useRef<Partial<StoreUpdaterProps>>(initPrevValues);
// these are values that are also passed directly to other components
// than the StoreUpdater. We can reduce the number of setStore calls
// by setting the same values here as prev fields.
translateExtent: infiniteExtent,
nodeOrigin: initNodeOrigin,
minZoom: 0.5,
maxZoom: 2,
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
});
useEffect( useEffect(
() => { () => {
@@ -1,14 +1,6 @@
import { forwardRef, type CSSProperties } from 'react'; import { forwardRef, type CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system';
ConnectionLineType,
PanOnScrollMode,
SelectionMode,
infiniteExtent,
isMacOs,
type NodeOrigin,
type Viewport,
} from '@xyflow/system';
import { A11yDescriptions } from '../../components/A11yDescriptions'; import { A11yDescriptions } from '../../components/A11yDescriptions';
import { Attribution } from '../../components/Attribution'; import { Attribution } from '../../components/Attribution';
@@ -18,9 +10,7 @@ import { useColorModeClass } from '../../hooks/useColorModeClass';
import { GraphView } from '../GraphView'; import { GraphView } from '../GraphView';
import { Wrapper } from './Wrapper'; import { Wrapper } from './Wrapper';
import type { ReactFlowProps, ReactFlowRefType } from '../../types'; import type { ReactFlowProps, ReactFlowRefType } from '../../types';
import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values';
export const initNodeOrigin: NodeOrigin = [0, 0];
const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
width: '100%', width: '100%',
@@ -89,11 +79,11 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
nodesDraggable, nodesDraggable,
nodesConnectable, nodesConnectable,
nodesFocusable, nodesFocusable,
nodeOrigin = initNodeOrigin, nodeOrigin = defaultNodeOrigin,
edgesFocusable, edgesFocusable,
edgesUpdatable, edgesUpdatable,
elementsSelectable = true, elementsSelectable = true,
defaultViewport = initDefaultViewport, defaultViewport = initViewport,
minZoom = 0.5, minZoom = 0.5,
maxZoom = 2, maxZoom = 2,
translateExtent = infiniteExtent, translateExtent = infiniteExtent,
@@ -0,0 +1,4 @@
import { type NodeOrigin, Viewport } from '@xyflow/system';
export const defaultNodeOrigin: NodeOrigin = [0, 0];
export const defaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
+1 -7
View File
@@ -303,13 +303,7 @@ const createRFStore = ({
set(currentConnection); set(currentConnection);
}, },
reset: () => { reset: () => set({ ...getInitialState() }),
// @todo: what should we do about this? Do we still need it?
// if you are on a SPA with multiple flows, we want to make sure that the store gets resetted
// when you switch pages. Does this reset solves this? Currently it always gets called. This
// leads to an emtpy nodes array at the beginning.
// set({ ...getInitialState() });
},
}), }),
Object.is Object.is
); );
+2 -2
View File
@@ -94,8 +94,8 @@ export function adoptUserProvidedNodes<NodeType extends NodeBase>(
...n, ...n,
computed: { computed: {
positionAbsolute: n.position, positionAbsolute: n.position,
width: n.computed?.width || currentStoreNode?.computed?.width, width: n.computed?.width,
height: n.computed?.height || currentStoreNode?.computed?.height, height: n.computed?.height,
}, },
}; };
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);